有没有办法在一个Ruby程序中多次启动OptionParser,每个程序都有不同的选项?
例如:
$ myscript.rb --subsys1opt a --subsys2opt b
这里,myscript.rb将使用subsys1和subsys2,将它们的选项处理逻辑委托给它们,可能是首先处理'a'的序列,然后是单独的OptionParser对象中的'b';每次选择仅与该上下文相关的选项。 最后阶段可以检查在每个部件处理完他们之后没有任何未知数。
用例是:
在一个松散耦合的前端程序中,各种组件有不同的参数,我不希望'main'知道所有内容,只是为每个部分委派参数/选项集。
将一些像RSpec这样的大型系统嵌入到我的应用程序中,我只需通过他们的选项传递一个命令行,而我的包装器不知道这些。
我也可以使用一些分隔符选项,例如某些Java应用程序中的--
或--vmargs
。
在Unix世界中有许多类似的东西的实际例子(startx / X,git plumbing和瓷器),其中一层处理一些选项但将其余部分传播到下层。
开箱即用,这似乎不起作用。每个OptionParse.parse!
调用都会进行详尽的处理,对任何它不知道的事情都会失败。
我想我很乐意跳过未知选项。
任何提示,也许是替代方法都是受欢迎的。
答案 0 :(得分:5)
我需要一个不会抛出OptionParser::InvalidOption
的解决方案,并且无法在当前答案中找到优雅的解决方案。这个猴子补丁基于other answers中的一个,但清理它并使其更像当前的order!
语义。但请参阅下文,了解多次传递选项解析所固有的未解决问题。
class OptionParser
# Like order!, but leave any unrecognized --switches alone
def order_recognized!(args)
extra_opts = []
begin
order!(args) { |a| extra_opts << a }
rescue OptionParser::InvalidOption => e
extra_opts << e.args[0]
retry
end
args[0, 0] = extra_opts
end
end
就像order!
一样,除了投掷InvalidOption
之外,它会在ARGV
中留下无法识别的开关。
RSpec测试:
describe OptionParser do
before(:each) do
@parser = OptionParser.new do |opts|
opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found << f }
end
@found = []
end
describe 'order_recognized!' do
it 'finds good switches using equals (--foo=3)' do
argv = %w(one two --foo=3 three)
@parser.order_recognized!(argv)
expect(@found).to eq([3])
expect(argv).to eq(%w(one two three))
end
it 'leaves unknown switches alone' do
argv = %w(one --bar=2 two three)
@parser.order_recognized!(argv)
expect(@found).to eq([])
expect(argv).to eq(%w(one --bar=2 two three))
end
it 'leaves unknown single-dash switches alone' do
argv = %w(one -bar=2 two three)
@parser.order_recognized!(argv)
expect(@found).to eq([])
expect(argv).to eq(%w(one -bar=2 two three))
end
it 'finds good switches using space (--foo 3)' do
argv = %w(one --bar=2 two --foo 3 three)
@parser.order_recognized!(argv)
expect(@found).to eq([3])
expect(argv).to eq(%w(one --bar=2 two three))
end
it 'finds repeated args' do
argv = %w(one --foo=1 two --foo=3 three)
@parser.order_recognized!(argv)
expect(@found).to eq([1, 3])
expect(argv).to eq(%w(one two three))
end
it 'maintains repeated non-switches' do
argv = %w(one --foo=1 one --foo=3 three)
@parser.order_recognized!(argv)
expect(@found).to eq([1, 3])
expect(argv).to eq(%w(one one three))
end
it 'maintains repeated unrecognized switches' do
argv = %w(one --bar=1 one --bar=3 three)
@parser.order_recognized!(argv)
expect(@found).to eq([])
expect(argv).to eq(%w(one --bar=1 one --bar=3 three))
end
it 'still raises InvalidArgument' do
argv = %w(one --foo=bar)
expect { @parser.order_recognized!(argv) }.to raise_error(OptionParser::InvalidArgument)
end
it 'still raises MissingArgument' do
argv = %w(one --foo)
expect { @parser.order_recognized!(argv) }.to raise_error(OptionParser::MissingArgument)
end
end
end
问题:通常OptionParser允许缩写选项,前提是有足够的字符来唯一标识预期的选项。多阶段解析选项会破坏这一点,因为OptionParser在第一遍中没有看到所有可能的参数。例如:
describe OptionParser do
context 'one parser with similar prefixed options' do
before(:each) do
@parser1 = OptionParser.new do |opts|
opts.on('--foobar=BAR', OptionParser::DecimalInteger) { |f| @found_foobar << f }
opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found_foo << f }
end
@found_foobar = []
@found_foo = []
end
it 'distinguishes similar prefixed switches' do
argv = %w(--foo=3 --foobar=4)
@parser1.order_recognized!(argv)
expect(@found_foobar).to eq([4])
expect(@found_foo).to eq([3])
end
end
context 'two parsers in separate passes' do
before(:each) do
@parser1 = OptionParser.new do |opts|
opts.on('--foobar=BAR', OptionParser::DecimalInteger) { |f| @found_foobar << f }
end
@parser2 = OptionParser.new do |opts|
opts.on('--foo=BAR', OptionParser::DecimalInteger) { |f| @found_foo << f }
end
@found_foobar = []
@found_foo = []
end
it 'confuses similar prefixed switches' do
# This is not generally desirable behavior
argv = %w(--foo=3 --foobar=4)
@parser1.order_recognized!(argv)
@parser2.order_recognized!(argv)
expect(@found_foobar).to eq([3, 4])
expect(@found_foo).to eq([])
end
end
end
答案 1 :(得分:4)
假设解析器运行的顺序定义良好,您可以将额外选项存储在临时全局变量中,并在每组选项上运行OptionParser#parse!
。
最简单的方法是使用你所提到的分隔符。假设第二组参数与分隔符--
的第一组参数分开。然后这将做你想要的:
opts = OptionParser.new do |opts|
# set up one OptionParser here
end
both_args = $*.join(" ").split(" -- ")
$extra_args = both_args[1].split(/\s+/)
opts.parse!(both_args[0].split(/\s+/))
然后,在第二个代码/上下文中,您可以执行以下操作:
other_opts = OptionParser.new do |opts|
# set up the other OptionParser here
end
other_opts.parse!($extra_args)
或者,这可能是“更合适”的方法,您可以简单地使用OptionParser#parse
,而不使用感叹号,这不会从{{1}中删除命令行开关} array,并确保两个集合中没有定义相同的选项。我建议不要手动修改$*
数组,因为如果你只是看第二部分,你的代码会更难理解,但你可以这样做。在这种情况下,您必须忽略无效选项:
$*
第二种方法实际上并不起作用,正如评论中所指出的那样。但是,如果您必须修改begin
opts.parse
rescue OptionParser::InvalidOption
puts "Warning: Invalid option"
end
数组,则可以改为:
$*
这不仅仅是一点点黑客攻击,但它应该做你想要的。
答案 2 :(得分:3)
对于后代,您可以使用order!
方法执行此操作:
option_parser.order!(args) do |unrecognized_option|
args.unshift(unrecognized_option)
end
此时,args
已被修改 - 所有已知选项都由option_parser
使用和处理 - 并且可以传递给其他选项解析器:
some_other_option_parser.order!(args) do |unrecognized_option|
args.unshift(unrecognized_option)
end
显然,这个解决方案依赖于顺序,但你要做的事情有点复杂和不寻常。
可能是一个很好的折衷方案是在命令行上使用--
来停止处理。这样做会使args
留下--
之后的任何内容,更多选项或常规参数。
答案 3 :(得分:2)
我遇到了同样的问题,我找到了以下解决方案:
options = ARGV.dup
remaining = []
while !options.empty?
begin
head = options.shift
remaining.concat(parser.parse([head]))
rescue OptionParser::InvalidOption
remaining << head
retry
end
end
答案 4 :(得分:2)
另一个解决方案依赖于parse!
对参数列表产生副作用,即使引发了错误。
让我们定义一个方法,它尝试使用用户定义的解析器扫描一些参数列表,并在抛出InvalidOption错误时递归调用自身,保存无效选项以便稍后使用最终参数:
def parse_known_to(parser, initial_args=ARGV.dup)
other_args = [] # this contains the unknown options
rec_parse = Proc.new { |arg_list| # in_method defined proc
begin
parser.parse! arg_list # try to parse the arg list
rescue OptionParser::InvalidOption => e
other_args += e.args # save the unknown arg
while arg_list[0] && arg_list[0][0] != "-" # certainly not perfect but
other_args << arg_list.shift # quick hack to save any parameters
end
rec_parse.call arg_list # call itself recursively
end
}
rec_parse.call initial_args # start the rec call
other_args # return the invalid arguments
end
my_parser = OptionParser.new do
...
end
other_options = parse_known_to my_parser
答案 5 :(得分:1)
我也需要同样的...它花了我一段时间,但最后一个相对简单的方法运作良好。
options = {
:input_file => 'input.txt', # default input file
}
opts = OptionParser.new do |opt|
opt.on('-i', '--input FILE', String,
'Input file name',
'Default is %s' % options[:input_file] ) do |input_file|
options[:input_file] = input_file
end
opt.on_tail('-h', '--help', 'Show this message') do
puts opt
exit
end
end
extra_opts = Array.new
orig_args = ARGV.dup
begin
opts.parse!(ARGV)
rescue OptionParser::InvalidOption => e
extra_opts << e.args
retry
end
args = orig_args & ( ARGV | extra_opts.flatten )
“args”将包含所有命令行参数,而不包含已经解析为“options”哈希的参数。我将这个“args”传递给一个外部程序,从这个ruby脚本中调用。
答案 6 :(得分:0)
当我编写一个包含ruby gem的脚本时遇到了类似的问题,该脚本需要自己的选项并传递参数。
我提出了以下解决方案,其中支持包含工具的参数。它通过第一个optparser解析它,并将它不能使用的内容分成一个单独的数组(可以用另一个optparse再次重新解析)。
optparse = OptionParser.new do |opts|
# OptionParser settings here
end
arguments = ARGV.dup
secondary_arguments = []
first_run = true
errors = false
while errors || first_run
errors = false
first_run = false
begin
optparse.order!(arguments) do |unrecognized_option|
secondary_arguments.push(unrecognized_option)
end
rescue OptionParser::InvalidOption => e
errors = true
e.args.each { |arg| secondary_arguments.push(arg) }
arguments.delete(e.args)
end
end
primary_arguments = ARGV.dup
secondary_arguments.each do |cuke_arg|
primary_arguments.delete(cuke_arg)
end
puts "Primary Args: #{primary_arguments}"
puts "Secondary Args: #{secondary_args}"
optparse.parse(primary_arguments)
# Can parse the second list here, if needed
# optparse_2.parse(secondary_args)
可能不是最好或最有效的方式,但它对我有用。
答案 7 :(得分:0)
我刚刚离开了Python。 Python ArgumentParser
有很好的方法parse_known_args()
。但它仍然不接受第二个论点,例如:
$ your-app -x 0 -x 1
首先-x 0
是您应用的参数。第二个-x 1
可以属于您需要转发的目标应用。在这种情况下,ArgumentParser
会引发错误。
现在回到Ruby,您可以使用#order
。幸运的是,它接受无限重复的参数。例如,您需要-a
和-b
。您的目标应用需要另一个-a
和强制参数some
(请注意,没有前缀-
/ --
)。通常#parse
将忽略强制参数。但是对于#order
,你会得到其余的 - 很棒。 注意您必须首先传递自己应用的参数 ,然后传递目标应用的参数。
$ your-app -a 0 -b 1 -a 2 some
代码应该是:
require 'optparse'
require 'ostruct'
# Build default arguments
options = OpenStruct.new
options.a = -1
options.b = -1
# Now parse arguments
target_app_argv = OptionParser.new do |opts|
# Handle your own arguments here
# ...
end.order
puts ' > Options = %s' % [options]
puts ' > Target app argv = %s' % [target_app_argv]
Tada: - )
答案 8 :(得分:0)
我的尝试:
def first_parse
left = []
begin
@options.order!(ARGV) do |opt|
left << opt
end
rescue OptionParser::InvalidOption => e
e.recover(args)
left << args.shift
retry
end
left
end
在我的情况下,我想扫描选项并选择可能设置调试级别,输出文件等的任何预定义选项。然后我将加载可能添加到选项的自定义处理器。加载完所有自定义处理器后,我调用@options.parse!(left)
来处理剩余的选项。请注意--help内置于选项中,因此如果您不想第一次识别帮助,则需要在创建OptParser之前执行'OptionParser :: Officious.delete('help')'然后添加自己的帮助选项
答案 9 :(得分:0)
解析选项直到第一个未知选项......可能会多次调用该块,因此请确保它是安全的...
options = {
:input_file => 'input.txt', # default input file
}
opts = OptionParser.new do |opt|
opt.on('-i', '--input FILE', String,
'Input file name',
'Default is %s' % options[:input_file] ) do |input_file|
options[:input_file] = input_file
end
opt.on_tail('-h', '--help', 'Show this message') do
puts opt
exit
end
end
original = ARGV.dup
leftover = []
loop do
begin
opts.parse(original)
rescue OptionParser::InvalidOption
leftover.unshift(original.pop)
else
break
end
end
puts "GOT #{leftover} -- #{original}"