Ever wanted to drive your ruby code with a CLI? Well, the commandable gem makes it easy.
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
And here's the output for a few representative cases:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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"] |