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).
- 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
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
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
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 takes a block that represents our main method from before.
go! calls that block, handling the exceptions for us. Our app now looks
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
Hash as well as the actual creation of the
Methadone provides two methods:
options provides access to a
Hash that we can use inside our
provides access to the underlying
OptionParser instance that is automatically created. We can now remove a few lines of code, losing no
1 2 3 4 5 6 7 8 9
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
OptionParser. You can still use
opts to access anything else, but for declaring command-line options, just call
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 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
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
on method. Suppose we only allowed usernames with all lower-case
characters? In Methadone, as in
OptionParser, you pass in a
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.
fatalgo 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=, 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
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