Command Line Apps with OptionParse

David Lyons
Share

Vector Command Line Icon

Command Line Applications may not be as common as they once were, but programmers use them just about every day. Even if you’re new to ruby you’ve probably used IRB and maybe some git (and if you haven’t yet, you will!) “Options”, “option flags” or just “flags” make interacting with these utilities faster, easier and more powerful, and Ruby is in on the fun.

When most people think of ruby they think of web apps and scripts. It’s true that ruby is awesome at both of these things, but you can use it to build command line utilities as well instead of using something like bash or C. Building the logic for options into your app from scratch every time would be a hassle, and fortunately, the standard ruby library includes a module called OptionParse that does the heavy lifting for us.

First let’s look at what an option is and what it does. “Options” and “arguments” look similar in a CLI but they are different. Here is a common git terminal command:

$ git commit -m "Initial commit"

In this example:

  • git is the application
  • commit is the command for the application to run
  • -m is the option flag
  • "Initial commit" is the argument passed to the command via the option

The above terminal command tells git to commit the currently staged files, and the -m flag let’s you type the commit message (notice how they used -m for “message”? clever!) all on one line. If you just type git commit into the terminal it will open an editor and ask for a commit message, along with some helper text. It looks something like this:

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
#
# Initial commit
#
# Changes to be committed:
# new file:   README.markdown

This is useful when you’re new to git. Chances are, however, that after you’ve made a few commits you don’t need to get flung into the editor and see this helper text time and time again. By using the -m option flag as we did above things are sped up considerably.

The OptionParse module makes it easy to create option flags and the helper text that explains to the user what each option does.

Because OptionParse is part of the standard library, you can add it to your project with

require 'optparse'

To show how the optparse library works, let’s take a look at a simple word manipulation tool I’ve written just for the occasion.

require 'optparse'

options = {}
OptionParser.new do |opts|
  opts.banner = "\nSimple word manipulator Ruby CLI app.\n"

  opts.on("-u", "--upcase", "Capitalize all letters") do |u|
    options[:upcase] = u
  end

  opts.on("-d", "--downcase", "Downcase all letters") do |d|
    options[:downcase] = d
  end

  opts.on("-c", "--capitalize", "Capitalize first character") do |c|
    options[:capitalize] = c
  end

  opts.on("-r", "--reverse", "Reverse the order of letters") do |r|
    options[:reverse] = r
  end

  opts.on("-h", "--help", "Displays help") do
    puts opts
    exit
  end
end.parse!

words = []

ARGV.each do |word|
  words << word.dup
end

words.each do |word|
  word.upcase! if options[:upcase]
  word.downcase! if options[:downcase]
  word.capitalize! if options[:capitalize]
  word.reverse! if options[:reverse]

  puts word
end

So, what does this code do? It takes arguments as strings from the command line, does one or more transformations, then puts the result to the terminal. Here’s what we get if we run this code with “Hello” and “World” as arguments, with the --reverse option:

$ ruby words.rb Hello World -r
#=> olleH
#=> dlroW

The first two lines don’t need much explanation: we require the optparse module and instantiate an empty hash called options.

require 'optparse'

options = {}

Jump past the OptionParse block and you see the ARGV global. This is where the arguments entered at the command line (but not the options, more on that in a second) are collected in an array. One special thing happening here: the arguments are all (#frozen)[http://www.ruby-doc.org/core-1.9.3/Object.html#method-i-freeze) which is why I’m copying the args into another array via the Object#dup method.

words = []

ARGV.each do |word|
  words << word.dup
end

The last block iterates over each word in the words array (a dup of the arguments entered at the command line), does the manipulations based on any options, and puts the result to the terminal.

words.each do |word|
  word.upcase! if options[:upcase]
  word.downcase! if options[:downcase]
  word.capitalize! if options[:capitalize]
  word.reverse! if options[:reverse]

  puts word
end

Finally, the magic. The code block from OptionParser.new do |opts| until end.parse! is where the options are defined, and at runtime, parsed from command line input. There are lots of conveniences built in to the OptionParser class:

  • OptionParser#banner lets you show arbitrary text to the user when they view help
  • OptionParser#on block lets you declare each of your options including the abbreviated flag (i.e. -u), the verbose flag (i.e. --upcase), the descriptive helper text (i.e. “Capitalize all letters”), and the behavior of the option
  • end.parse! intelligently pulls the option flags out of the argument list, leaving the actual arguments to be handled by your code. That means, if you need multiple flags, you can do any of the following and still get the same output:
    • $ ruby words.rb Hello World -r -u
    • $ ruby words.rb -r Hello World -u
    • $ ruby words.rb -ru Hello World

Let’s take one last look at the OptionParser.new block:

OptionParser.new do |opts|
  opts.banner = "\nSimple word manipulator Ruby CLI app."

  opts.on("-u", "--upcase", "Capitalize all letters") do |u|
    options[:upcase] = u
  end

  opts.on("-d", "--downcase", "Downcase all letters") do |d|
    options[:downcase] = d
  end

  opts.on("-c", "--capitalize", "Capitalize first character") do |c|
    options[:capitalize] = c
  end

  opts.on("-r", "--reverse", "Reverse the order of letters") do |r|
    options[:reverse] = r
  end

  opts.on("-h", "--help", "Displays help") do
    puts opts
    exit
  end
end.parse!

In our example, each option is just being used as a boolean, which is a common usage, but you could do pretty much anything you want. In a simple app I wrote for a previous job, most of my option flags added or removed file extensions from a global array before the rest of the code was executed.

This example command line app works (copy and paste it into your favorite text editor and try it out!), but it can be improved. For one thing, certain flags cannot be run together because of the order in which they are checked or the way the methods change the string. How would you change this code to make it more robust and more flexible? What happens if you have strings with special characters? How many arguments can you send before the code blows up (and, if there is a limit, are you likely to reach it hand typing into the terminal?) Let me know in the comments!

CSS Master, 3rd Edition