Command Line Applications for the Rest of Us

Tweet

cliI vaguely remember the first time I had to use the command line. It’s a scary step for any would be developer. You leave a safe, friendly, graphical environment for a bleak and unforgiving landscape. It’s just one of those things. After you have been drinking the Kool-Aid for a while you cannot understand how you lived without being on the terminal.

IDE’s and graphical clients become nothing short of clumsy and “Oh how slow?” can they be! The command line quickly becomes a badge of honour for us nerds. You could be just grep-ing over a bunch of files and any ignorant bystanders think you are hooked into The Matrix or something.

As Ruby developers our first intro to the language was more than likely a terminal environment. I still drop into irb if I need to check a bit of syntax, idiom or even just to use all the power of Ruby as a simple calculator.

With that in mind I still feel command line applications are somewhat second class citizens. More often than not they are nothing more than quickly mashed together scripts, so we have to scan the source for instructions of how to use them. We are all spoiled with Rails and Sinatra making it easy to create weird and wonderful interfaces that enhance the experience for non-technical and technical users alike.

However, if you are doing something exclusively for geeks, the command line is where I want it to live. We take for granted common tools such as git, Rails generators, and Rake tasks. Rarely do we consider the time and effort invested in crafting such sophisticated command line suites.

Giving CLI Some Love

Getting the interface right is vital. The key ingredients of the command line interface are commands (duh!), flags (or options), and of course arguments. We will suck eggs for a bit and explain these by example.

Commands

The most obvious is the command itself. If we are talking git, pull, push and commit are all commands. We are telling the git application we want to perform some specific action.

Flags or Options

At their most basic level these flags tell the command to execute with some non-default behaviour. For example, the *nix mkdir command can take a -p or --parents flag. Specified here in both long and short formats. They do the same thing only one is more explicit to read.

By adding -p with a path will create the hierarchy specified whether the parents exist or not. Another example of this would be the common -v or --verbose flag. These are almost classified as global states for the duration of the commands execution. I tend to mix and match what format of flag I use.

Looking at the examples I have just given, for the mkdir parent flag I always use the short form, -p, but whenever I’m looking for lots of verbose output I always opt for the long format. There is no particular reason, as I said they are exactly the same, just one format tends to feel “more right” depending on the circumstance.

It is worth mentioning that the application and command can take independent options. For example I’ll stick with git, as pretty much everyone reading this article will be using or at least have used it. Git itself can take the flag -p or --paginate which will pipe any output from the execution into less. Also the commit command takes -a and -m or even combined -am to stage all modified files and commit with the subsequent message. We can combine the whole thing into git -p commit -am 'A well formed commit message'. Clearly showing an application flag and command options for the git suite, albeit a really silly example. Don’t use -p for commits.

Arguments

The third and final ingredient to our CLI is of course the arguments we wish to pass to the command in question. A good example of this would be ls /some/directory, the directory path being the arguments for which the command will work with.

A Ruby Tinted CLI

The first step in creating a command line app with Ruby is to create what I call a bootstrap file. This is the executable that acts as the first step a user takes into our application. Achieving this is as simple as creating a text document, setting the permissions and adding the correct shebang. As a one liner it looks like:

touch test && chmod +x test && echo "#!/usr/bin/env ruby" >> test

When we execute ./test, nothing happens. Feel free to fill it with puts statements to check its all OK.

All the good command line apps I know are all very modular. They are single responsibility at it’s finest. Git, again for example, a single commit is actually a whole load of other actions plumbed into a single command phrase for convenience.

Now for a naive pass at a command line app we will use Ruby’s ARGV. The test file we created earlier will implement the following to get us started.

ARGV.each { |arg| puts arg }

Now executing ./test hello 'hiya you' goodbye will output:

hello
hiya you
goodbye

I only do this to show the arguments are split and assigned into the ARGV array, as well as the effect of using quotes.

Before we go any further, are you beginning to dread parsing ARGV for flags, commands, command options and arguments? I certainly am.

Fortunately Ruby ships with a pretty nifty library to remove any array walking/parsing headaches. This of course is the OptionParser standard library.

As an example we are going to emulate the fantastic todo app, todo.txt. As the name suggests, it simply stores your todo items in a text file and allows you to assign priorities, context, and projects in a single command. Our version will be much simpler (from a message parsing point of view) and only assign priority for the item and it’s message. Our interface will look like:

./todo -p high -m 'Take out the garbage'`

In the above statement we are assigning a ‘high’ priority to the the todo item Take out the garbage. First we will look at the code and explain later.

#!/usr/bin/env ruby

>require 'optparse'
require 'ostruct'

options = OpenStruct.new
OptionParser.new do |opts|

  opts.on('-h', 'Shows this help screen') do
    puts opts
    exit
  end

  opts.on('-p', '--priority PRIORITY', 'Set the items priority') do |priority|
    options.priority = priority
  end

  opts.on('-m', '--message MESSAGE', 'Set the todo item') do |message|
    options.message = message
  end

end.parse!

puts options.priority
puts options.message

Breaking this down, first off we require the libraries optparse and ostruct. The OpenStruct isn’t actually required for this application to function, a simple hash would suffice, but I like to use OpenStruct for ease of access and assignment for my options. Then we create an instance of OptionParser which takes a block. Within this block we define what options the app and its commands will take.

The first is the help screen which does nothing but provide some instructions of how to use the app. The next two are a bit more interesting, as we ask these options to parse the content following them as PRIORITY and MESSAGE respectively. These arguments are duly set up as attributes in our options struct.

We finally call parse! on our OptionParser object and spit the options to STDOUT. To boot, by examining the output of the -h flag we get a short description of what is required input for the application.

Tightening Input

The OptionParser#on method is extremely flexible. Say we wanted to restrict priorities to a given set of ‘high’ to ‘low’ we can fix this using:

opts.on('-p', '--priority PRIORITY', [:high, :medium, :low], 'Set the items priority') do |priority|
  options.priority = priority
end

Another common use is to make the options have defaults to save some keystrokes. With OptionParser again this is trivial:

opts.on('-p', '--priority [PRIORITY]', [:high, :medium, :low], 'Set the items priority') do |priority|
  options.priority = priority || :medium
end

Simply by enclosing the argument in braces we have made it optional, all the user has to do is pass the -p flag. In most cases I have not found this type of defaulting very useful. It really only works well when dealing with file locations. For example, the user decides (normal operation is no logging) they want to log to a file be it in a default location or otherwise. For our todo app, all items will be a priority of medium unless specified differently.

To achieve this we will have to look outside OptionParser and the answer is to initialize the options struct with our defaults attributes.

defaults = { priority: :medium }
options = OpenStruct.new(defaults)

I especially like that we defin the help text as we build the interface for our command line application. Sure, it’s not quite as refined as something like Rake’s desc but what you have with plain old Ruby is pretty powerful.

A Splash of Color

Want to add some output to the terminal? Feedback is essential for our users to know what they have just typed in to that scary command line has actually done something fruitful. Of course we could always just puts and we will. But under normal operation, that will just fire back text in the color the users profile settings have specified. For our todo application we want, at the very least, to return a success message and a colorised view of the item based on it’s priority. Red for high, and so on.

For a first pass, we can look at just using plain old ANSI codes to format the text `puts “e[31m#{options.priority}e[0m”. But lets be honest, by the time you crack the horrible ANSI codes, you will have given up on your command line app and made it cloud based.

Thankfully, community gems come to the rescue with a whole host of colorizing libraries at our disposal, all primed to drop in at will. Probably the most well known is term-ansicolor. However, these days for sheer simplicity of syntax, I tend to lean more towards colored where we can achieve nifty syntax: puts "HIGH PRIORITY".red.

Wrapping up the Command Line

So far we have set up a command line interface and added a splash of color to make it even more appealing. If we want to get into the internals of adding todo items we can simply hook into Ruby classes which will be nice and test driven. It is certainly true OptionParser will be more than adequate for most simple command line applications, but as your suite of tools continues to grow you will find it becoming cumbersome and painful.

Again the wealth of gems in the ruby community is our savior. We have already discussed Rake, but we have not looked at the ‘other’ command line tool included in standard Rails installs. The Thor gem that is similar in syntax to Rake but places more emphasis on coding via conventions, creating commands in Ruby classes that inherit from Thor. A similar DSL approach to creating command line interfaces is to use the GLI gem. All of these tools simply mean that CLI’s have no excuse for being second class applications.

If you do it well, you get a really flexible and long serving weapon in your development workflow. If you do it great? Well then it becomes a part of the Ruby community.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • David Porter

    Great article but there is a typo on the OpenStruct initialization hash syntax:

    defaults = { :priority => :medium }

  • http://arrc.in arrC

    Hi Dave,
    I really liked your demonstration of ruby’s cli abilities, however it would have been more helpful if you would have provided the link to source code of the above example.