From: Robert Klemme Date: 2007-10-25T22:15:22+09:00 Subject: Re: overcoming dynamic block evaluation 2007/10/25, Alexy Khrabrov : > 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 Khrabrov on 2007-10-24. > # Copyright (c) 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 Unfortunately I don't have time to go through all of this, but did you consider to define a block with the default options like this? DEFAULT_OPTS = lambda do |opts| opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options.verbose = v end ... end ... opts = OptionParser.new do |opts| DEFAULT_OPTS[opts] opts.on("--type [TYPE]", [:text, :binary, :auto], "Select transfer type (text, binary, auto)") do |t| options.transfer_type = t end end Kind regards robert