Overcoming dynamic block evaluation

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

2007/10/25, Alexy K. [email protected]:

but the opt.on(…) { block } is triggered only by the actual option.

   }
       help  = "run verbosely"
       help="directory location for the output files"
       help="more dirs with aliases, #{code_list}"

   puts "#{o}"

no = {:moredir => 1}
puts “verbose => #{o.verbose}”
The idea was that each option has a name, by which it is referred to

{ |val| @o[:verbose] = val }
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

end
Well, I need to add textual reporting and report on all options, both
set and unset. By “default” options I mean not those common to all
scripts, but those which are not mentioned explicitly on the command
line, thus their blocks never get triggered. So we cannot add
reporting lines there as puts. In my solution reporting lines are
added to an enclosing unless statement, and they are added
to a list which is walked in the reporting loop later.

The question really is about overcoming the fact that the opt.on
blocks are evaluated later and my initial trick of referring to
options on a uniform way as o stumbles upon that deferred evaluation,
when o was long redefined.

Cheers,
Alexy