overcoming dynamic block evaluation


A

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
 
Ad

Advertisements

R

Robert Klemme

2007/10/25 said:
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
 
Ad

Advertisements

A

Alexy Khrabrov

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

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 <disabled> 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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top