10 January 2012

Awesome CLI gem: Commandable

Ever wanted to drive your ruby code with a CLI?  Well, the commandable gem makes it easy.

At first, I struggled with OptionParser in the stdlib, but I had to duplicate my option handling across CLI code and method invocation.  Then OptionParser didn't handle required parameters at all, requiring me to do required parameters in a non-DRY way.   Also, OptionParse doesn't do sub-commands, git-style, which I needed for the new git plugin I'm working on.

When I saw the pickled_optparse gem (from Mike Bethany), it appeared to solve the missing 'required parameter' feature.  And the subcommand gem seemed to allow for subcommands with their own options, git-style.  However, the duplication across option definition and method invocation was still present.  And even subcommand required you to hook up the command to the class/method that needed to be called.

Finally I found that the author of pickled_optparse had totally rethought CLI in ruby with his commandable gem.  And I really like what he did.

Now there is no real additional logic AT ALL in order to hook your ruby class up to the command line.  Now my option handling code will be in the place it belongs, in the code that is doing the command that I invoked from the command line.

The main awesomeness here is that commandable pushes the CLI code into the appropriate place in your ruby class -- it makes it hard to scatter CLI code all over the place.

My only beef with the gem are the following points:

  • it comes with colors enabled by default (unusual for a CLI)
  • it clears the screen by default when displaying help (when colors are enabled, very unusual and annoying for a CLI)
  • it appears to be hard to do global options like: "cmd --global subcommand [args]"
  • there is no support for parsing command CLI-style options into ruby hashes, but this is a minor nit, because that's what OptionParser is for, and it's easy to use inside the method


I fixed a problem where the screen was cleared even if you disabled colors, and I've pushed it up to my fork.

Here is my hello world that shows commandable in action:
require 'commandable'
require 'optparse'
class Foo
extend Commandable
# the :default param is optional, and can only be on one method
command "hello", :default
def hello(world="world") # the default value is expressed inline in real ruby
puts "hello #{world.inspect}"
raise_on_invalid_world world
end
def raise_on_invalid_world(world)
raise FriendlyError, world if world =~ /^nowhere/
raise RuntimeError, world if world =~ /^never/
end
# lets me do full commandline parsing in the place the option will be used
command "hello2", :rest
def hello2(*args)
opts = OptionParser.new
options = { world: "world" }
opts.on "-w WORLD", "--world WORLD" do |world|
options[:world] = world
end
opts.order! args
puts "hello2 #{options[:world].inspect}, args: #{args.inspect}"
raise_on_invalid_world options[:world]
end
end
class FriendlyError < RuntimeError
def friendly_name
"bogus place, #{message}"
end
end
Commandable.color_output = false
Commandable.execute ARGV, silent: true
view raw foo.rb hosted with ❤ by GitHub

And here's the output for a few representative cases:
[08:41:35] ~ ↺ ruby foo.rb
hello "world"
[08:41:39] ~ ↺ ruby foo.rb hello
hello "world"
[08:41:41] ~ ↺ ruby foo.rb hello world2
hello "world2"
[08:41:43] ~ ↺ ruby foo.rb hello nowhere
hello "nowhere"
Error: bogus place, nowhere
nowhere
Usage:
Command Parameters Description
hello [world="world"] : hello (default)
hello2 *args : hello2
help : you're looking at it now
[08:41:56] ~ ↺ ruby foo.rb hello never
hello "never"
#<RuntimeError: never>
foo.rb:16:in `raise_on_invalid_world'
foo.rb:11:in `hello'
/home/sumsionjg/.rbenv/versions/1.9.3-p0/gemsets/gitged-experiments/gems/commandable-0.2.3/lib/commandable/commandable.rb:245:in `block (2 levels) in execution_queue'
...snip...
foo.rb:42:in `<main>'
[08:42:22] ~ ↺ ruby foo.rb hello2 never
hello2 "world", args: ["never"]
[08:42:34] ~ ↺ ruby foo.rb hello2 --world=world2 never
hello2 "world2", args: ["never"]
view raw output.txt hosted with ❤ by GitHub

No comments:

Post a Comment