Greetings – I’m writing a reusable options module for my scripts.
All scripts in the same project share most of the common command-line
options. I’m relying on the optparse module to do it. The desired
functionality is a report on all options being used, structured so
that it’s also defined incrementally next to each options, and
possibility to disable some of the reusable options and add some new
ones.
This is problem with optparse: how to report all
options in verbose mode, including the defaults for those not set –
but the opt.on(…) { block } is triggered only by the actual option.
Moreover, it’s evaluated in the parse() call context, which makes
using variables in those blocks problematic. Consider the following
toy program I wrote to develop a better, reusable option processing.
#!/usr/bin/env ruby
Created by Alexy K. on 2007-10-24.
Copyright © 2007. All rights reserved.
require ‘optparse’
DIR_CODES = %w[central subdir shotgun same cwd]
DIR_CODE_ALIASES = { :gun => :shotgun, :working => :cwd }
class Options
def initialize(no=nil,xdefs=nil,&block)
@o = { # default values
:file_ext => “.txt”,
:dir_kind => :shotgun,
:dirout => “kuku”
}
# inject new defaults, if any – hash add, h1+h2?
xdefs.each { |k,v| @o[k] = v } if xdefs
opt = nil
report = []
begin
files = OptionParser.new do |opt|
opt.banner = "Preved!"
unless no[o=:verbose]
help = "run verbosely"
short = "-v"
# trying to replace late-binding below with
ref_o = lambda { o } # but ref_o would be different in
another opt block!
opt.on(short, “–[no-]#{o}”, help) { |val| @o[:verbose] =
val }
report << [o,short,help]
end
unless no[o=:dirout]
help="directory location for the output files"
short = "-d"
opt.on(short, "--#{o} DIR",
DIR_CODES.map { |x| x.to_sym }, help) { |val| @o[:dirout]
= val }
report << [o,short,help]
end
code_list = (DIR_CODE_ALIASES.keys + DIR_CODES).map{|x|
x.to_sym}.join(’,’)
unless no[o=:moredir]
help=“more dirs with aliases, #{code_list}”
short = “-m”
opt.on(short, “–#{o} DIR”,
DIR_CODES.map{|x|x.to_sym}, DIR_CODE_ALIASES, help) { |
val| @o[o] = val }
report << [o,short,help]
end
yield opt, @o, report if block
end.parse(*ARGV) # parse it! remainder -> files
report.each do |o,short,help|
val = @o[o] || "unset"
STDERR.printf "--%s\t%s,\t%s: %s\n", o, short, help, val
end
rescue OptionParser::InvalidOption => o
puts "#{o}"
puts opt.help
exit(1)
rescue OptionParser::AmbiguousArgument => o
puts "#{o}"
exit(1)
end
end # initialize
def method_missing (m, *a, &block)
@o[m.to_sym]
end
end # Options
def main
no = {:moredir => 1}
xdefaults = { :extra => “hurry to see!” }
puts
o = Options.new(no, xdefaults) do |opt,hash,report|
name = :extra
help = “extra option, added in the new script”
short = “-x”
opt.on(short,"–#{name} X",help) { |val| hash[:extra] = val }
report << [name,short,help]
end
puts
puts “verbose => #{o.verbose}”
puts “file_ext => #{o.file_ext}”
puts “dir_kind => #{o.dir_kind}”
puts “dir_more => #{o.dir_more}”
puts “extra => #{o.extra}”
end
if $0 == FILE
main
end
The idea was that each option has a name, by which it is referred to
in an internal hash @o. The hash parameter called `no’ contains those
options from the preexistng Options instance, which we want to disable.
Additional options are defined in the block given to Options.new;
their defaults are passed as a hash to that constructor. Also one can
override predefined defaults through the same extra defaults hash.
So far so good.
But notice the assignment in disablement check,
unless no[o=:verbose]
– I planned on using o uniformly later, perhaps abstracting more out
of my newly uniform opt.on sections. Yet I was getting an obscure
error there when the block looked like
{ |val| @o[o] = val }
– o turned out to be the name of the option from the last opt.on
block! All these blocks are evaluated later, in parse(). So I had to
write
{ |val| @o[:verbose] = val }
explicitly. Played with lambda and Proc, yet they, too, are only
evaluated later. We want to get at that very o, but we can’t even
stick a binding into the block, as it’ll be only evaluated later!
Notice we can stick o into the parameter strings given to each
opt.on() as they are evaluated right away. I was wondering about
getting the name back from an internal value for long, yet it requires
getting into optparse internals and is not a clean solution to
overcoming the dynamic binding in this case.
Is there a way to “constantize” o in @o[o], when o==:verbose, to be
equivalent to @o[:verbose] – replacing variable reference by it
literal value in a location in the program text?
BTW, Python’s optparse allows default definitions…
Cheers,
Alexy