Really Cheap Command-Line Option Parsing in Ruby

I share your distaste for require 'getopts', mainly due to the awesomeness that is OptionParser:

% cat temp.rb                                                            
require 'optparse'
OptionParser.new do |o|
  o.on('-d') { |b| $quiet = b }
  o.on('-i') { |b| $interactive = b }
  o.on('-f FILENAME') { |filename| $filename = filename }
  o.on('-h') { puts o; exit }
  o.parse!
end
p :quiet => $quiet, :interactive => $interactive, :filename => $filename
% ruby temp.rb                                                           
{:interactive=>nil, :filename=>nil, :quiet=>nil}
% ruby temp.rb -h                                                        
Usage: temp [options]
    -d
    -i
    -f FILENAME
    -h
% ruby temp.rb -d                                                        
{:interactive=>nil, :filename=>nil, :quiet=>true}
% ruby temp.rb -i                                                        
{:interactive=>true, :filename=>nil, :quiet=>nil}
% ruby temp.rb -di                                                       
{:interactive=>true, :filename=>nil, :quiet=>true}
% ruby temp.rb -dif apelad                                               
{:interactive=>true, :filename=>"apelad", :quiet=>true}
% ruby temp.rb -f apelad -i                                              
{:interactive=>true, :filename=>"apelad", :quiet=>nil}

Here's the standard technique I usually use:

#!/usr/bin/env ruby

def usage(s)
    $stderr.puts(s)
    $stderr.puts("Usage: #{File.basename($0)}: [-l <logfile] [-q] file ...")
    exit(2)
end

$quiet   = false
$logfile = nil

loop { case ARGV[0]
    when '-q' then  ARGV.shift; $quiet = true
    when '-l' then  ARGV.shift; $logfile = ARGV.shift
    when /^-/ then  usage("Unknown option: #{ARGV[0].inspect}")
    else break
end; }

# Program carries on here.
puts("quiet: #{$quiet} logfile: #{$logfile.inspect} args: #{ARGV.inspect}")

Since nobody appeared to mention it, and the title does refer to cheap command-line parsing, why not just let the Ruby interpreter do the work for you? If you pass the -s switch (in your shebang, for example), you get dirt-simple switches for free, assigned to single-letter global variables. Here's your example using that switch:

#!/usr/bin/env ruby -s
puts "#$0: Quiet=#$q Interactive=#$i, ARGV=#{ARGV.inspect}"

And here's the output when I save that as ./test and chmod it +x:

$ ./test
./test: Quiet= Interactive=, ARGV=[]
$ ./test -q foo
./test: Quiet=true Interactive=, ARGV=["foo"]
$ ./test -q -i foo bar baz
./test: Quiet=true Interactive=true, ARGV=["foo", "bar", "baz"]
$ ./test -q=very foo
./test: Quiet=very Interactive=, ARGV=["foo"]

See ruby -h for details.

That must be as cheap as it gets. It will raise a NameError if you try a switch like -:, so there's some validation there. Of course, you can't have any switches after a non-switch argument, but if you need something fancy, you really should be using at the minimum OptionParser. In fact, the only thing that annoys me about this technique is that you'll get a warning (if you've enabled them) when accessing an unset global variable, but it's still falsey, so it works just fine for throwaway tools and quick scripts.

One caveat pointed out by FelipeC in the comments in "How to do really cheap command-line option parsing in Ruby", is that your shell might not support the 3-token shebang; you may need to replace /usr/bin/env ruby -w with the actual path to your ruby (like /usr/local/bin/ruby -w), or run it from a wrapper script, or something.


As the author of Trollop, I cannot BELIEVE the stuff that people think is reasonable in an option parser. Seriously. It boggles the mind.

Why should I have to make a module that extends some other module to parse options? Why should I have to subclass anything? Why should I have to subscribe to some "framework" just to parse the command line?

Here's the Trollop version of the above:

opts = Trollop::options do
  opt :quiet, "Use minimal output", :short => 'q'
  opt :interactive, "Be interactive"
  opt :filename, "File to process", :type => String
end

And that's it. opts is now a hash with keys :quiet, :interactive, and :filename. You can do whatever you want with it. And you get a beautiful help page, formatted to fit your screen width, automatic short argument names, type checking... everything you need.

It's one file, so you can drop it in your lib/ directory if you don't want a formal dependency. It has a minimal DSL that is easy to pick up.

LOC per option people. It matters.

Tags:

Ruby