Tagged With   gem:name=boson , lib:name=irb , post:lang=ruby , post:type=tutorial

Boson - Command Your Ruby Universe - Part Deux

With the latest release of Boson, there are a slew of new features that need some explaining. Yes, there is a changelog and documentation, but some of these features are novel enough that a good intro is in order.

Overview

Here’s an overview of features:

Command Options

Boson is unique among command frameworks for providing commands with global options. Global options are options that are common to all commands (that enable options). There are three types of global options: basic options, render options and pipe options. The first two were covered in this previous post. Now let’s a take a look at the newcomer, pipe options.

Pipe Options

Whereas render options generate different views from a command’s return value, pipe options pipe a command’s return value to command(s). Since a pipe option maps directly to a command, using piping options is much like piping between unix commands but asynchronously.

For example, say we have pipe options --browser and --copy which given a url, open it in a browser and copy it to clipboard respectively:

  bash> irb
  # Assume a command generate_url which returns a url string
  >> generate_url
  => 'http://example.com'

  # With the assumed global pipe options, let's apply them to the return value of generate_url
  >> generate_url '-bC'   #  or generate_url '--browser --copy'
  => 'http://example.com'

  # The above one liner is functionally equivalent to the following three lines
  >> url = generate_url
  >> browser(url)
  >> copy(url)

  # In a unix shell assuming boson commands are shell commands we could do the above as
  bash> generate_url | browser
  bash> generate_url | copy

Now that you understand what pipe options do, here’s how you set it up:

  # Let's set up the commands for mac osx:
  # browser() already ships with boson.

  # Drop this in a library that gets loaded with :defaults config key
  def copy(str)
    IO.popen('pbcopy', 'w+') {|clipboard| clipboard.write(str)}
  end

  # Configure them in ~/.boson/config/boson.yml
  :pipe_options:
    :browser:
      :type: :boolean
      :desc: Open in browser
    :copy:
      :type: :boolean
      :desc: Copy to clipboard

As you’ve noticed, pipe options are returning the command’s original return value. To have pipe options behave like unix pipes (i.e. the return value changes per pipe), add a :filter: true to a pipe option’s config. See here for more pipe docs.

Query Option

As explained at the end of a previous post, this option used to be --query_fields. This option is a default pipe option that queries an array of anything with a search hash. Since this option is a :hash option type, check the docs for a syntax overview. Some examples with default commands:

  # A query field is delimited from its query value with ':'
  # Like arrays, hash keys can be aliased if the field keys are known.
  bash> boson commands -q=f:lib     # or commands --query=full_name:lib

  # With the new format all fields can be queried using a '*'.
  # Searches library fields: gems,dependencies,commands,loaded,module,name,namespace,indexed_namespace,library_type
  bash> boson libraries -q=*:core   # or libraries --query=*:core

  # Multiple searches to be joined together by ','. This query searches for libraries that
  # have the name matching core or a library_type matching gem.
  bash> boson libraries -q=n:core,l:gem   # or libraries --query=name:core,library_type:gem

For examples of the different kinds of arrays that query can search, see the previous post.

Libraries

Boson’s libraries, which are just modules, are loaded by being included in main ‘s eigenclass. During its loading process, a module’s include callbacks are called i.e. included as well as boson-specific ones i.e. after_included. Read the docs for an explanation of these callbacks.

Local Libraries

Like most command frameworks, Boson now supports local command libraries. For just one library, simply create a Bosonfile and write commands as you would for any Boson::FileLibrary. For multiple libraries, create the directory lib/boson/commands or .boson/commands and place libraries under the directory. For more, see the docs.

Directory-Spaced Libraries

Like Thor, Boson has a central repository for its libraries so that the user can invoke commands from any directory. For Boson, these libraries are stored at ~/.boson/commands/. With this release, any subdirectory in this directory is automatically namespaced when loaded. What this means for the user is that they can create any number/depth of directories without worry that a module in one directory will conflict with a module in another. As an example say that I use the module Misc in two different directories, ~/.boson/commands/public and ~/.boson/commands/personal. Boson will load Misc as Boson::Commands::Public::Misc and Boson::Commands::Personal::Misc respectively. This makes it very convenient to collect and organize a large number of libraries. If it means anything, my main repository’s current count is 87 libraries and 284 commands.

Social Libraries

As I explained before, Boson’s libraries are easy to share with others. Simply give a url to a plain text version of your library and other users can install it. What I didn’t explain was that you can customize any attribute of the library and its commands without touching the installed library.

For this example, we’ll use the github library. To install it:

  bash> boson install https://github.com/cldwalker/irbfiles/raw/master/boson/commands/public/site/github.rb
  Saved to /Users/bozo/.boson/commands/github.rb

  # How one of the library's commands looks before we customize
  bash> boson commands user_repo
  +------------+--------+-------+------------------------------------------------+--------------------------------+
  | full_name  | lib    | alias | usage                                          | description                    |
  +------------+--------+-------+------------------------------------------------+--------------------------------+
  | user_repos | github |       | [--user=cldwalker] [--fork_included] [--stats] | Displays a user's repositories |
  +------------+--------+-------+------------------------------------------------+--------------------------------+
  1 row in set
  

Let’s add an alias and change the options and description of the above user_repos command:

  # Add entry under the :libraries key of config file ~/.boson/config/boson.yml
  :libraries:
    github:
      :commands:
        user_repos:
          :alias: guser
          :description: Github user page
          :options:
            :user: rails
    
  # Reindex library to pick up the config change
  bash> boson -i=github commands user_repo  # or --index=github commands user_repo
  Indexing the following libraries: github
  Loaded library github
  +------------+--------+-------+--------------------------------------------+------------------+
  | full_name  | lib    | alias | usage                                      | description      |
  +------------+--------+-------+--------------------------------------------+------------------+
  | user_repos | github | guser | [--user=rails] [--fork_included] [--stats] | Github user page |
  +------------+--------+-------+--------------------------------------------+------------------+
  1 row in set

  # Calls user_repos defaulting to rails user
  bash> boson guser
  +-------------------------+----------+-------+-----------------------------------+-------------------------------------------+
  | name                    | watchers | forks | homepage                          | description                               |
  +-------------------------+----------+-------+-----------------------------------+-------------------------------------------+
  | rails                   | 4468     | 718   | http://rubyonrails.org            | Ruby on Rails                             |
  | account_location        | 82       | 2     | http://rubyonrails.org            | Account Location Plugin                   |
  | acts_as_list            | 251      | 28    | http://rubyonrails.org            | ActsAsList plugin                         |
  | acts_as_nested_set      | 43       | 7     | http://rubyonrails.org            | ActsAsNestedSet                           |
  # ...

You’ve just seen how Boson can customize command invocation via :alias and functionality via :options without modifying the command’s source. Since commands are functionally equivalent to scripts, this has interesting implications for scripting. This basically allows us to use and preserve third-party scripts without being bound to the author’s naming/aliasing of commands and options. By preserving a script’s source, we can update our third-party scripts when new versions come out. Perhaps Boson’s script/command configuration will make scripting more social.

Invoking Commands

If you don’t already know, Boson’s commands are just methods on main, Ruby’s top-level object. What this means is that the self inside of any Boson command is main. Once a command is loaded, it’s available across libraries as a top-level method. For example:

  # Drop this in ~/.boson/commands/input.rb
  module Input
    def ask(prompt)
      print prompt + ' '
      gets.strip
    end
  end

  # Now let's use this in another library
  module Quarantine
    # A boson callback to specify a library's configuration
    def self.config
      {:dependencies=>['input']}
    end

    # Opens a reddit url in browser. Is it really worth it?
    def reddit(url)
      if ask("Are you sure you want to do this to yourself? (y/N)")[/^y/i]
        browser(url)
      end
    end
  end

As you can see, there is no Rake::Task['reddit'].invoke(url) or Quarantine.new.invoke(:reddit, url) as with Rake and Thor respectively. Invoking commands from different libraries is done in plain ruby per Boson’s philosophy. With this philosophy, you can still use these commands without Boson.

  >> obj = Object.new.extend(Input).extend(Quarantine)
  >> obj.reddit('lovely reddit url')

Library Dependencies

In the above command invocation example, you probably noticed that the quarantine library depends on the input library. Boson supports libraries depending on other libraries. This begins to show the collaborative and swappable nature of Boson libraries. Swappable in that you could swap one library for another as a dependency as long as it implements the same methods and api. Collaborative in that the functionality defined in one library can be made available to any another without modifying any code. Personally, this is is an improvement over having a directory full of scripts that can’t communicate and depend on each other.

Plugin Libraries

While Boson has a number of features, you may want to add/modify some of them. No problem. Boson’s config file has a :defaults key to load up specified Boson libraries at startup. From the Invoking Commands section, if we wanted to make the ask method available to all libraries, we would specify the input library under :defaults. For more examples, see my plugins.

Option Parser

With this release, Boson’s option parser is available for any Ruby scripts. For example:

#!/usr/bin/env ruby

require 'rubygems'
require 'boson'

# Define options and parse them from ARGV into a hash
options = Boson::OptionParser.parse(:verbose=>:boolean, :times=>:numeric, :help=>:boolean)
return puts("$0 #{Boson::OptionParser.usage}") if options[:help]

# Process remaining args in ARGV ...

Since Boson has dependencies, I’d consider making Boson’s OptionParser a separate gem if requested.

Option to Object Mapper

With this release, Boson’s option parser has become an option-to-object mapper. To understand what that means, let’s see Boson’s five default option types and the Ruby objects they create. First, we create a library:

  # Drop in ~/.boson/commands/option_test.rb
  module OptionTest
    options :a=>:array, :b=>:hash, :c=>:string, :d=>:numeric, :e=>:boolean
    def testin(options={})
      p options
      p options.each {|k,v| options[k] = v.class }
    end

    # The default for :hash has to be passed as a :default attribute since hashes
    # are already reserved for setting option attributes.
    options :a=>[1,2], :b=>{:default=>{:a=>1}}, :c=>'yo', :d=>1.3, :e=>false
    def testin2(options={})
      p options
      p options.each {|k,v| options[k] = v.class }
    end
  end

Then, back in the shell:

  # Each option type maps to a different ruby object. The classes of most of these objects
  # match directly to the option type's name.
  bash> boson testin -a=1,2 -b a:b,c:d -c=hey -d 1 -e
  {:e=>true, :d=>1, :b=>{"a"=>"b", "c"=>"d"}, :c=>"hey", :a=>["1", "2"]}
  {:e=>TrueClass, :d=>Fixnum, :b=>Hash, :c=>String, :a=>Array}

  # Each option type has an expected format it uses for converting to an object as indicated
  # by the usage.
  bash> boson testin -h
  testin [-c=C] [-d=N] [-e] [-a=A,B,C] [-b=A:B,C:D]

  # When options are defined with default values, the parser uses the default value's class to determine
  # the appropriate option type.
  # As expected, whatever option isn't set has its default value set.
  bash> boson testin2 -b=z:y -d=2.1
  {:e=>false, :d=>2.1, :b=>{"z"=>"y"}, :c=>"yo", :a=>[1, 2]}
  {:e=>FalseClass, :d=>Float, :b=>Hash, :c=>String, :a=>Array}  

As you can see, an option type maps to objects of one or more Ruby classes i.e. :array → Array and :numeric → Float or Fixnum. But what if you want to create objects of other classes?

Custom Option Types

To define an option type that creates objects of your choice, you only need to define the method that creates the object. Here’s a basic example to create a :date option type that creates Date objects:

  # Drop this in ~/.boson/commands/date.rb
  # Then make this library a plugin by putting it
  # under :defaults key in the main config
  module ::Boson::Options::Date
    def create_date(value)
     Date.parse(value + "/#{Date.today.year}")
    end
  end

  ::Boson::OptionParser.send :include, ::Boson::Options::Date

  # ====
  # Now in any library we can use the :date option type ...
  # Drop this in ~/.boson/commands/date_test.rb
  module DateTest
    options :day=>:date
    def day_of_week(options={})
      puts "#{options[:day]} falls on a #{Date::DAYNAMES[options[:day].cwday]}"
    end
    # to define :day with a default we could modify options to be:
    # options :day=>Date.today
  end

Now to try day_of_week:

  bash> boson day_of_week -d 12/25   # or day_of_week --date 12/25
  2009-12-25 falls on a Friday

  # Default usage for new option type
  bash> boson day_of_week -h
  day_of_week [--day=:date]

As you saw, simply creating a method with the name create_@type in the option parser made the option type available. If you want to add validation or custom usage for a new option type, see the docs. When creating your own option types, you may find it cleaner to define your options in a plugin library.

Miscellaneous

Now for some miscellaneous features.

Custom Consoles

Being able to start irb around certain setups you use often can be quite handy. Good examples of this are Rails’ script/console and the more recent racksh. Boson makes it easy to start irb with specified Boson libraries loaded:

  # Assuming you have a library github
  bash> boson -l=g -c   # or boson --load=github --console

  # Assuming you have libraries misc and ruby_ref
  bash> boson -l=m,r -c  # or boson --load=misc,ruby_ref --console
  
  # Irb can require libraries in a more verbose way but it *doesn't*
  # load Boson libraries as commands
  bash> irb -I ~/.boson/commands -r misc -r ruby_ref

Powerful One-Liners

Much like ruby -e, you can do a boson -e with any ruby code. The big difference is that any Boson command is available and loaded lazily automatically:

  # Drop in ~/.boson/commands/objects.rb
  module Objects
    def objects(klass)
      object = []
      ObjectSpace.each_object(klass) {|e| object.push(e) }
      object
    end
  end

  # Prints number of String objects
  bash> boson -e 'p objects(String).size'  # or boson --execute 'p objects(String).size'
  56573

  # Prints default classes not including its defaults
  bash> boson -e 'p objects(Class).select {|e| e.to_s !~ /^(Boson|Alias|Hirb)/ }'
  [Gem::LoadError, Struct::Group, Struct::Passwd, SizedQueue, Queue, ConditionVariable, Mutex, Gem::Builder, Gem::SourceIndex, Binding,
   UnboundMethod, Method, Proc, SystemStackError, LocalJumpError, Struct::Tms, Process::Status, Time, Dir, File::Stat, File, IO, EOFError,
   IOError, Range, MatchData, Regexp, RegexpError, Struct, Hash, Array, Errno::EDQUOT, Errno::ESTALE, Errno::EINPROGRESS, Errno::EALREADY
  # ...

Method Commandification

This feature redefines a method to act like a shell command using Boson::Scientist.commandify. This means that a method can take any number of arguments and options as one string. Since Boson commands already do this automatically, this feature is for those who want to give this functionality to any method:

  >> def checkit(*args); args; end
  => nil
  >> Boson::Scientist.commandify(self, :name=>'checkit', :options=>{:verbose=>:boolean, :num=>:numeric})
  => ['checkit']
  # regular invocation
  >> checkit 'one', 'two', :num=>13, :verbose=>true
  => ["one", "two", {:num=>13, :verbose=>true}]
  # commandline invocation
  >> checkit 'one two -v -n=13'
  => ["one", "two", {:num=>13, :verbose=>true}]
Enjoyed this post? Tell others! hacker newsHacker News | twitterTwitter | DeliciousDelicious | redditReddit
blog comments powered by Disqus