--noooooooooooooooooo

Apr 28, 2018

Ruby’s OptionParser surprised me the other day.


Say you want to make a CLI in Ruby. Ruby comes with an OptionParser and so you think “hey, this might be handy for parsing CLI arguments.”

# pizza.rb
require 'optparse'

class Options
  attr_accessor :sauce

  def initialize
    @sauce = 'marinara'
  end
end

options = Options.new

OptionParser.new do |parser|
  parser.on '--sauce=SAUCE' do |sauce|
    options.sauce = sauce
  end
end.parse!

puts "sauce: #{options.sauce}"

Let’s test it out.

$ ruby pizza.rb --sauce=pesto
sauce: pesto
$ ruby pizza.rb
sauce: marinara

OptionParser only calls the block passed to on if the flag is in the ARGV. It calls the block with the value the user passes to the flag.

OptionParser.new do |parser|
  parser.on '--use-marinara' do |use_marinara|
    puts "Use marinara sauce? so #{use_marinara}"
  end
end.parse!
$ ruby pizza.rb --use-marinara
Use marinara sauce? so true
$ ruby pizza.rb
$ 

OptionParser makes a boolean switch out of long arguments with no declared value. It calls the on block with the boolean if the switch is in the ARGV.


I don’t think adding the word “no” to flags is a good idea. But what happens when we do?

OptionParser.new do |parser|
  parser.on '--no-onions' do |no_onions|
    puts "NO ONIONS???: EXTREMELY #{no_onions.to_s.upcase}--<<<"
  end
end.parse!
$ ruby pizza.rb --no-onions
NO ONIONS???: EXTREMELY FALSE--<<<

OptionParser negates the boolean! Didn’t expect that.

OK, what if I just hack it by declaring it as a flag with a value?

OptionParser.new do |parser|
  parser.on '--no-sauce=NO_SAUCE' do |no_sauce|
    puts "--no-sauce=#{no_sauce}"
  end
end.parse!
$ ruby pizza.rb --no-sauce=1
/poop.rb:25:in `<main>': needless argument: --no-sauce=1 (OptionParser::NeedlessArgument)

Hm. Maybe OptionParser still thinks that it’s a switch.

$ ruby pizza.rb --no-sauce
--no-sauce=false

Yep.

Let’s take a look at the OptionParser docs. It shows a minimal example:

OptionParser.new do |opts|
  opts.banner = "Usage: example.rb [options]"

  opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options[:verbose] = v
  end
end.parse!

Aha! --[no-]verbose. Declaring an optional [no-] allows both positive and negative versions of the switch.

OptionParser.new do |parser|
  parser.on '--[no-]onions' do |onions|
    puts "onions? #{onions}"
  end
end.parse!
$ ruby pizza.rb --onions
onions? true
$ ruby pizza.rb --no-onions
onions? false

OK, the boolean negation makes sense here. OptionParser eases the burden of writing two switches if you want to have a negative version of some positive option. For example, it helps us avoid writing something like:

OptionParser.new do |parser|
  parser.on '--onions' do |onions|
    puts "onions? #{onions}"
  end

  parser.on '--no-onions' do |onions|
    puts "onions? #{onions}"
  end
end.parse!

Why does declaring the non-optional --no-onions behave like the optional --[no-]onions? I don’t know, but I imagine that the optparse authors wanted to keep behavior consistent between optional and non-optional no-s.

(This has been the behavior in OptionParser since at least 2002.)