I’ve spent the last year writing a book on building awesome command-line applications in Ruby. Over the course of writing it, I’ve used a lot of Ruby libraries for building command-line apps, and none of them work quite right. In my book, I spent significant time on OptionParser, since it’s builtin, and GLI, since I wrote it (and since it’s actually very fully-featured compare to the alternatives).
I just finished up an appendix where I showed alternate implementations of the running examples using main, thor, and trollop. I did this for a few reasons:
- These tools are popular, and people have asked if they’d be included
- They are, by and large, very different from how
OptionParserand GLI work - I wanted to give them a real shakedown
I also surveyed many other tools, but, alas, I couldn’t include everything. Each of these tools have a common theme, which is to
avoid the boilerplate of OptionParser, and make it really easy to parse command-line arguments. They all have done this, but at
a cost. All of them are less powerful and extensible than OptionParser, and only slightly more compact (or, in the case of
main, more verbose).
Enter methadone, which has all of OptionParser’s power, but the compactness of these other frameworks.
Another command-line option parser?
Yes and no. Methadone isn’t a re-implementation of command-line option parsing. It’s barely a DSL, making use of almost no
meta-programming, class_eval, or other craziness. It’s a plain Ruby proxy to OptionParser, with some helper methods. It makes
idiomatic option parsing and command-line app design as seemless as possible, but doesn’t force any of itself on
you. In this post, I’ll derive its syntax while showing you the basics of how to structure a simple command-line app.
You’ll have to buy the book to dig deeper1.
Basic Command-line App Structure
Most command-line apps start off with parsing the command-line with OptionParser (which typically consists of setting values into
some Hash), defining a few helper methods, and then, at the end, implementing the main logic of the program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Yuck. The boilerplate option parsing is bad enough, but the structure is all wrong. The interesting stuff is all the way at the bottom; you have to read the thing in the wrong order. At the very least, you should extract the core logic into a main method, put that at the top, and call it at the end.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
Now, we can see, immediately upon opening the file, the main thing this app is doing.
Of course, an exception might be raised. We may even do it on purpose, but we can’t have the app vomiting a stack trace to the user, so we wrap our call to main in a begin..rescue block:
1 2 3 4 5 6 | |
Methadone’s Main Method
The structure we just saw is pretty decent, and gives us, and future contributors, an easy way to follow the code. Users also get a pretty decent experience and never have to see a backtrace.
This brings us to the first feature of methadone. Instead of including this boilerplate every time, we extract it into a module,
Methadone::Main, which gives us two methods: main and go!.
main takes a block that represents our main method from before. go! calls that block, handling the exceptions for us. Our app now looks
like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
go! will extract the contents of ARGV leftover after parsing and pass them to the block. Since they’re passed as individual arguments, you don’t have to call shift a bunch of times on some array. Just name your parameters whatever, and Metahdone takes care of it. If your main block raises an exception, go! will handle catching it, messaging the user without a backtrace, and exiting nonzero2.
Parse Options with no Loss of Power
Notice how we can still safely use OptionParser. Methadone doesn’t hide that. As we’ll see, it provides some more features to make option
parsing even easier. First, we can get rid of the options Hash as well as the actual creation of the OptionParser instance.
Methadone provides two methods: options and opts. options provides access to a Hash that we can use inside our main block. opts
provides access to the underlying OptionParser instance that is automatically created. We can now remove a few lines of code, losing no
functionality:
1 2 3 4 5 6 7 8 9 | |
Given that opts is baked in, there’s no need to even use that for our cases, because Methadone provides a method on that proxies to the
underlying OptionParser. You can still use opts to access anything else, but for declaring command-line options, just call on
directly:
1 2 3 4 5 6 7 8 9 | |
You can see, as we peel off layers of boilerplate, Methadone hides nothing; it’s just making commonly-written code easier to write. At any time, you can abandon it and go back to the old way.
So far, we’ve only saved a few lines of code and a couple of characters. That’s because we haven’t seen the true power of the on method.
on is more than just a proxy to OptionParser. It does one additional thing for us: it we omit the block, Methadone will provide one
for us. That Methadone-provided block simply sets the value from the command-line in the
options Hash automatically. Meaning that the above code is equivalent to this:
1 2 3 4 | |
Not bad! This means that all we need to do, assuming we’re doing things idiomatically, is to give on the names of our options and their
descriptions. Note, however, this still proxies to OptionParser’s on method. Suppose we only allowed usernames with all lower-case
characters? In Methadone, as in OptionParser, you pass in a Regexp:
1 2 | |
Suppose you want the value type-converted for you? We have access to the underlying OptinParser, so we can set that up easily:
1 2 3 4 5 6 | |
Do the Right Thing
You’ve noticed that we are still setting our banner manually. You’ve also noticed our banner is kinda lame; It doesn’t say what our app does nor does it give an overview of how to use it. It should look like so:
$ awesome_app.rb --help
Does so many awesome things, you won't believe it.
Usage: awesome_app.rb [options] thing other_thing [optional_thing]
Since Methadone knows that our app takes options (by virtue of us having declared them), and it knows the name of our app, we just need to tell it what our app does, and it will assemble the banner for us3.
1 2 3 4 5 6 7 8 9 10 | |
Finally, you’ll note that our main block takes three arguments. Methadone provides the method arg that allows us to name them (in the language the user will understand) and indicate which are required and which are optional. Methadone will put this information into the banner, and will fail if any required arguments are missing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Now, the banner looks like we’d like it, and we didn’t have to do much more than describe our program. You can
even bootstrap your app using the methadone command-line app. It will create an empty app, using this structure, with
some helpful comments to let you describe your UI easily and quickly. But it won’t prevent you from doing any sort of crazy thing with
OptionParser that you need to.
Sweet, Sweet Sugar
But wait! There’s more! Complex programs start to look like this:
1 2 3 4 5 6 7 8 9 | |
You’ve got a mix of commented-out debug statements, informational messages and tediously long statements sending error messages to the
standard error. Methadone includes a special Logger instance, along with some helper methods, that does away with all this:
1 2 3 4 5 6 7 8 9 10 11 | |
The logger is set up as follows:
debugmessages don’t go anywhere.infogoes to the standard output.warn,error, andfatalgo to the standard error.- Log messages are unformatted when logged to a TTY
- Log messages are formatted with timestampes, levels, etc, when logged to a file
This means that for command-line use, the user sees messages formatted for them, and not horrible Maven-style enterprise logging. As soon as
you use your app in cron, however, the logger senses the absence of a TTY and switches its format to this style, so that the log files do
have that valuable information.
You have complete access to the logger via logger and logger=, so you can ultimatley do whatever you want.
Methdone::CLILogging is included in Methdone::Main, so, if you followed the structure above, you have access to the logger and these
methods.
Is there more?
In addition to all of this, Methadone provides some Cucumber step definitions, based on Aruba that allow you to
test-drive your command-line app. When you bootstrap your app using methadone, this will be set up for you.
I’m planning a few more things before v1.0.0, so checkout the roadmap for more info.
And, don’t forget the buy the book