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

Boson - Command Your Ruby Universe

Introducing Boson, a command/task framework that could change how you collect and execute your ruby code. Sure, there’s rake, thor and a dozen other gems. But how many will let you create a universe of ruby commands you can run from the commandline and irb?

higgs boson decay

Spinning On

Feel free to follow Boson on the Hub and to install it:

  $ gem install boson boson-more
  $ echo "require 'boson/more'" >> ~/.bosonrc

To get an idea of Boson’s features, here’s an outline:

Shell and Irb Duality

Like traditional command frameworks, Boson executes commands(tasks) from the commandline:

  # Let's list boson's default libraries
  bash> boson libraries
  +----------+----------+------+--------------+
  | name     | commands | gems | library_type |
  +----------+----------+------+--------------+
  | core     | 6        |      | module       |
  | web_core | 3        |      | module       |
  +----------+----------+------+--------------+
  2 rows in set

  # And default commands
  bash> boson commands
  +--------------+----------+-------+--------------------------------------------+-----------------------------------------------------------------------------+
  | full_name    | lib      | alias | usage                                      | description                                                                 |
  +--------------+----------+-------+--------------------------------------------+-----------------------------------------------------------------------------+
  | usage        | core     |       | [name][--verbose]                          | Print a command's usage                                                     |
  | libraries    | core     |       | [query=''][--index] [--query_fields=A,B,C] | List or search libraries                                                    |
  | render       | core     |       | [object] [options={}]                      | Render any object using Hirb                                                |
  | load_library | core     |       | [library][--verbose] [--reload]            | Load/reload a library                                                       |
  | commands     | core     |       | [query=''][--index] [--query_fields=A,B,C] | List or search commands                                                     |
  | menu         | core     |       | [output] [options={}] [&block]             | Provide a menu to multi-select elements from a given array                  |
  | get          | web_core |       | [url]                                      | Gets the body of a url                                                      |
  | install      | web_core |       | [url][--force] [--name=NAME]               | Installs a library by url. Library should then be loaded with load_library. |
  | browser      | web_core |       | [*urls]                                    | Opens urls in a browser                                                     |
  +--------------+----------+-------+--------------------------------------------+-----------------------------------------------------------------------------+
  9 rows in set
  => true

But unlike others, you can do the same in the ruby console:

  bash> irb
  # You could drop this line in your irbrc
  >> require 'boson'; Boson.start
  Loaded library core
  Loaded library web_core
  => nil

  >> libraries
  +----------+----------+------+--------------+
  | name     | commands | gems | library_type |
  +----------+----------+------+--------------+
  | core     | 6        |      | module       |
  | web_core | 3        |      | module       |
  +----------+----------+------+--------------+
  2 rows in set
  => true

  >> commands
  # same as above ...

How is this done? Commands are simply methods on Ruby’s top level object, main. You provide the methods and Boson manages and empowers them. Even with more complex command usage, the shell/irb duality holds:

  # Print basic command usage in irb
  irb>> commands '-h'
  commands [query=''][--index]
  => nil
  # Or in your shell
  bash> boson commands -h
  # same as above ...
  
  # Search command fields full_name and description for 'lib' and sort by full_name
  irb>> commands 'full_name,description:lib --sort full_name'
  +--------------+----------+-------+-------------------------------------------+-----------------------------------------------------------------------------+
  | full_name    | lib      | alias | usage                                     | description                                                                 |
  +--------------+----------+-------+-------------------------------------------+-----------------------------------------------------------------------------+
  | install      | web_core |       | [url][--force] [--name=NAME]              | Installs a library by url. Library should then be loaded with load_library. |
  | libraries    | core     | lib   | [query=''][--index] [--query_fields=name] | List or search libraries                                                    |
  | load_library | core     | ll    | [library][--verbose] [--reload]           | Load/reload a library                                                       |
  +--------------+----------+-------+-------------------------------------------+-----------------------------------------------------------------------------+
  3 rows in set
  => true

  # In your shell
  bash> boson commands full_name,description:lib --sort full_name
  # same as above ...

  # Using option aliases
  bash> boson commands lib -s f -q=f,d
  # same as above ...

  # Execute a command repo_search under namespace github in irb
  irb>> github.repo_search 'crazy'
  # or in a shell
  bash> boson github.repo_search crazy

Creating Commands

Creating a Boson command is as easy as opening a module and defining a method:

  # Drop this in ~/.boson/commands/brain.rb
  # The module name can be anything but it makes sense to name it the same as the file.
  # The module is evaluated under Boson::Commands
  module Brain
    # Help Brain live his dream
    def take_over(destination)
      puts "Pinky, it's time to take over the #{destination}!"
    end
  end

Let’s give the new command a spin:

  # Unfortunately Brain can't do much right now
  bash> boson take_over farm
  Pinky, it's time to take over the farm!

  # Of course we now have a brain library
  bash> boson libraries
  +----------+----------+------+--------------+
  | name     | commands | gems | library_type |
  +----------+----------+------+--------------+
  | core     | 6        |      | module       |
  | web_core | 3        |      | module       |
  | brain    | 1        |      | file         |
  +----------+----------+------+--------------+
  3 rows in set

  # And a take_over command
  bash> boson commands take
  +-----------+-------+-------+---------------+---------------------------+
  | full_name | lib   | alias | usage         | description               |
  +-----------+-------+-------+---------------+---------------------------+
  | take_over | brain |       | [destination] | Help Brain live his dream |
  +-----------+-------+-------+---------------+---------------------------+
  1 row in set

  # And of course you can execute this all from irb ...

As you can see, Boson lets you write your libraries and commands in plain ruby. Module-method DSLs be damned. If you want more commands for the brain library, add more methods to Brain in the file. Also notice that Boson automatically generates a command’s usage and description from the method’s arguments and the comment above it.

So that’s cool. Boson’s libraries are just modules and its commands just methods. But if this is all the functionality Boson offers, why bother with it at all?

Commands With Options

Borrowing from Thor, Boson commands can have simple yet powerful options. Let’s add some to take_over():

  module Brain
    options :execute=>:boolean, :countdown=>:numeric
    # Help Brain live his dream
    def take_over(destination, options={})
      puts "Pinky, it's time to take over the #{destination}!"
      sleep(options[:countdown]) if options[:countdown]
      system("/home/brain/take_over/#{destination}") if options[:execute]
    end
  end

Brain always has the best of intentions:

  bash> boson take_over world --execute --countdown=3
  Pinky, it's time to take over the world!
  # program sleeps 3 seconds
  # executes `/home/brain/take_over/world`

  # Thanks to auto-generated option aliases
  bash> boson take_over world -e -c3
  # same as above ...

  # Boson still holds true to its irb duality
  bash> irb
  >> take_over 'world -e -c3'
  # same as above ...

  # And if you want, call your command in plain ruby
  >> take_over 'world', :execute=>true, :countdown=>3

A few points:

  • options() takes a hash of options with names mapping to option types
  • Short flags or option aliases are automatically generated based on the first letter of the option name.
  • Options have plenty of functionality.
  • Commands with options must expect their last argument to be an options hash.

So it’s great that options can be defined concisely, but whatever happened to just writing commands in plain ruby? Well, Boson actually lets you but it’s gonna cost you 2 more characters:

  module Brain
    #@options :execute=>:boolean, :countdown=>:numeric
    # Help Brain live his dream
    def take_over(destination, options={})
    # ....
  end
As you can see, I commented out the DSL method and prepended ‘options’ with ‘@’. This command still has the same functionality while still keeping it all in plain ruby.

Just Alias It

Built with the power user in mind, Boson encourages aliasing of commands, options and even option values. Let’s take a look at each.

Aliasing Commands

Aliasing commands is done through Boson’s main config file at ~/.boson/config/boson.yml. For an example config file, see mine. Let’s add command aliases for the default command commands and our example command take_over:

  # Drop this in ~/.boson/config/boson.yml
  :command_aliases:
    commands: com
    take_over: take

  # Back in the shell
  # List commands with 'com' or 'take in its name:
  bash> boson -l=brain com com\|take
  +-----------+-------+-------+------------------------------------------------+---------------------------+
  | full_name | lib   | alias | usage                                          | description               |
  +-----------+-------+-------+------------------------------------------------+---------------------------+
  | commands  | core  | com   | [query=''][--index] [--query_fields=full_name] | List or search commands   |
  | take_over | brain | take  | [destination]                                  | Help Brain live the dream |
  +-----------+-------+-------+------------------------------------------------+---------------------------+
  2 rows in set

  # And if we wanted to execute take_over with the new alias
  bash> boson take world -e -c3

Aliasing Options

As explained above, options automatically have aliases generated for them. But if you want, specify your own aliases in options():

  # Let's add some aliases to the execute option
  
  module Brain
    #@options [:execute, :conquer, :x]=>:boolean, :countdown=>:numeric
    # ...
  end

  # The execute option can now be called with -x and --conquer

Aliasing Option Values

Aliasing option values is possible for option types :string and :array. But unlike the other aliases, you specify a unique string that a value starts with rather than define explicit aliases. Let’s redo take_over() to take a :string option with some expected values:

  module Brain
    #@options :destination=>{:type=>:string, :values=>%w{world farm fjord}},
    # :execute=>:boolean, :countdown=>:numeric
    # Help Brain live his dream
    def take_over(options={})
      puts "Pinky, it's time to take over the #{options[:destination]}!"
      sleep(options[:countdown]) if options[:countdown]
      system("/home/brain/take_over/#{options[:destination]}") if options[:execute]
    end
  end

As you can see we added a :destination option with a :values attribute. With this attribute, the :destination option is able to instantly alias those values. For more about the :values attribute see here. Let’s see this aliasing in action:

  # 'f' will match 'farm' over 'fjord' since values are searched alphabetically
  bash> boson take -d=f
  Pinky, it's time to take over the farm!

  # It's still easy to get fjord
  bash> boson take -d=fj
  Pinky, it's time to take over the fjord!

  # This is what Brain really wants to do
  bash> boson take -d=w -e
  Pinky, it's time to take over the world!

  # If at any time you need to remember your option's values
  bash> boson -hv take
  Loaded library core
  Loaded library web_core
  Loaded library brain
  take [--execute] [--destination=DESTINATION] [--countdown=N]

  COMMAND OPTIONS
  +---------------+-------+---------+------------------+
  | Option        | Alias | type    | Values           |
  +---------------+-------+---------+------------------+
  | --countdown   | -c    | numeric |                  |
  | --destination | -d    | string  | world,farm,fjord |
  | --execute     | -e    | boolean |                  |
  +---------------+-------+---------+------------------+
  # ...

You may be pleased to know that the default commands libraries and commands come with all this goodness:

  bash> irb
  >> commands '-hv'
  commands [query=''][--index]

  COMMAND OPTIONS
  +----------------+-------+---------+----------------+------------------------------------------------------------------------+
  | Option         | Alias | type    | Description    | Values                                                                 |
  +----------------+-------+---------+----------------+------------------------------------------------------------------------+
  | --index        | -i    | boolean | Searches index |                                                                        |
  +----------------+-------+---------+----------------+------------------------------------------------------------------------+

  GLOBAL/RENDER OPTIONS
  +----------------+-------+---------+-----------------------------------------------------------+------------------------------------------------------------------------+
  | Option         | Alias | type    | Description                                               | Values                                                                 |
  +----------------+-------+---------+-----------------------------------------------------------+------------------------------------------------------------------------+
  | --class        | -c    | string  | Hirb helper class which renders                           |                                                                        |
  | --fields       | -f    | array   | Displays fields in the order given                        | name,lib,alias,description,options,args,usage,full_name,render_options |
  | --help         | -h    | boolean | Display a command's help                                  |                                                                        |
  | --max_width    | -m    | numeric | Max width of a table                                      |                                                                        |
  | --pretend      | -p    | boolean | Display what a command would execute without executing it |                                                                        |
  | --render       | -r    | boolean | Toggle a command's default rendering behavior             |                                                                        |
  | --reverse_sort | -R    | boolean | Reverse a given sort                                      |                                                                        |
  | --sort         | -s    | string  | Sort by given field                                       | name,lib,alias,description,options,args,usage,full_name,render_options |
  | --verbose      | -v    | boolean | Increase verbosity for help, errors, etc.                 |                                                                        |
  | --vertical     | -V    | boolean | Display a vertical table                                  |                                                                        |
  +----------------+-------+---------+-----------------------------------------------------------+------------------------------------------------------------------------+
 => true

  # Don't worry about the other options. To be covered in another blog post :p

So yes, you can search commands and libraries while choosing which fields to search, sort and display with aliased options and option values!

Using Third Party Commands

Boson makes it dead easy to install someone else’s commands:

  # Let's download a library which explains irb's default commands
  bash> boson install https://github.com/cldwalker/irbfiles/raw/master/boson/commands/public/irb_core.rb
  Saved to /Users/bozo/.boson/commands/irb_core.rb

  # Let's see the commands in irb
  bash> irb
  >> commands '-q=l irb_core'
  +-------------------------------+----------+------------+-------+-----------------------------------------------------------------------------+
  | full_name                     | lib      | alias      | usage | description                                                                 |
  +-------------------------------+----------+------------+-------+-----------------------------------------------------------------------------+
  | irb_pop_workspace             | irb_core | popws      |       | Pops current workspace and changes to next workspace in context             |
  | irb_require                   | irb_core |            |       | Evals file like require line by line                                        |
  | public                        | irb_core |            |       | Works same as module#public                                                 |
  | private                       | irb_core |            |       | Works same as module#private                                                |
  | irb                           | irb_core |            |       | Starts a new workspace/subsession                                           |
  | irb_push_workspace            | irb_core | pushws     |       | Creates a workspace for given object and pushes it into the current context |
  | irb_load                      | irb_core |            |       | Evals file like load line by line                                           |
  | irb_change_workspace          | irb_core | cws        |       | Changes current workspace to given object                                   |
  | irb_source                    | irb_core | source     |       | Evals full path file line by line                                           |
  | irb_jobs                      | irb_core | jobs       |       | List workspaces/subsessions                                                 |
  | irb_fg                        | irb_core | fg         |       | Switch to a workspace/subsession                                            |
  | irb_help                      | irb_core | help       |       | Ri based help                                                               |
  | irb_kill                      | irb_core | kill       |       | Kills a given workspace/subsession                                          |
  | include                       | irb_core |            |       | Works same as module#include                                                |
  | irb_exit                      | irb_core | exit       |       | Kills the current workspace/subsession                                      |
  | irb_workspaces                | irb_core | workspaces |       | Array of workspaces for current context                                     |
  | irb_context                   | irb_core | conf       |       | Displays configuration for current workspace/subsession                     |
  | install_alias_method          | irb_core |            |       | Aliases given method, allows lazy loading of dependent file                 |
  | irb_current_working_workspace | irb_core | cwws       |       | Prints current workspace                                                    |
  +-------------------------------+----------+------------+-------+-----------------------------------------------------------------------------+
  19 rows in set
  => true

  # Sweet! Now we have a list and description of commands that come with irb.  

If you remember, Boson’s libraries are just modules. So any url that points to a ruby module now offers you a library of Boson commands. You can even have the installer automatically wrap ruby code in a method and a module to make it a valid Boson library.
For example, take this gist for displaying authors on a git repository:

Let’s commandify it:

  bash> boson install https://gist.github.com/raw/203861/c063260bef7f004c9db4c2a7719ff649b59d3ade/git-authors -m
  Saved to /Users/bozo/.boson/commands/git_authors.rb.
  
  # Verify we have the command
  bash> boson commands auth
  +-------------+-------------+-------+-------+-------------+
  | full_name   | lib         | alias | usage | description |
  +-------------+-------------+-------+-------+-------------+
  | git_authors | git_authors |       |       |             |
  +-------------+-------------+-------+-------+-------------+
  1 row in set

  # Running this in my local repo of http://github.com/wycats/thor
  bash> boson git_authors
  * José Valim
  * Nathan Weizenbaum
  * Yehuda Katz
  * Brian Donovan
  * Fabien Franzen
  * Mislav Marohnić
  * James Herdman
  * Charles Jolley
  * Markus Prinz
  * Luis Lavena
  * Damian Janowski
  * Jack Dempsey
  * valodzka
  * Gabriel Horner
  * Edwin Moss

To try some third-party Boson libraries, here are some of mine I recommend.

Namespaces

If you’re familiar with rake and thor, you may be surprised that there has been no mention of namespaces. That’s because by default they’re optional. Boson, like Ruby, assumes you’re grown up enough to balance power and peril. To aid with keeping the default namespace clean, Boson doesn’t load a library if it introduces a command which will conflict with existing commands. This conflict detector will even catch third-party gems who pollute the default namespace by monkeypatching Kernel or Object. You can turn off the conflict detector per library as needed. If Boson’s conflict detector isn’t comforting enough, no worries. You can configure any library to have a namespace. And if you yearn for namespacing by default, simply drop :auto_namespace: true into your ~/.boson/config/boson.yml.

Spinning Off

Although we’ve covered most of Boson’s basics, we still haven’t touched on how Boson integrates with Hirb. That’s for the next post.

And for all you physics geeks out there:

  >> Boson.higgs.instance_eval("class << self; self end").ancestors[1]
  => Boson::Universe

UPDATE: The format for querying commands and libraries has changed since this was posted. The above examples have been updated to reflect this. Examples:

  # With the old format fields to query were passed to --query_fields
  bash> boson commands lib --query_fields=full_name --sort=full_name   # or commands lib -q=f -s=f

  # With the new format, fields are placed before a query with a ':'
  # --query_fields doesn't exist
  bash> boson commands full_name:lib --sort=full_name     # or commands f:lib -s=f

  # The new format still maintains the old format of querying with an implicit query field.
  # The implicit field is 'name' for libraries.
  bash> boson libraries core     # same as libraries name:core

  # 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 *:core

  # The new format allows for 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 name:core,library_type:gem
Enjoyed this post? Tell others! hacker newsHacker News | twitterTwitter | DeliciousDelicious | redditReddit
blog comments powered by Disqus