Может ли OptionParser пропускать неизвестные параметры, которые будут обработаны позже в программе Ruby?
Есть ли способ запустить OptionParser несколько раз в одной программе Ruby, каждый с разными наборами опций?
Например:
$ myscript.rb --subsys1opt a --subsys2opt b
Здесь myscript.rb будет использовать subsys1 и subsys2, делегируя им свою логику обработки опций, возможно, в последовательности, где сначала обрабатывается "a", а затем "b" в отдельном объекте OptionParser; каждый раз выбор вариантов, релевантных только для этого контекста. На последнем этапе можно было проверить, что после обработки каждой части ничего не осталось.
Варианты использования:
-
В слабосвязанной интерфейсной программе, где различные компоненты имеют разные аргументы, я не хочу, чтобы "главное" знать обо всем, просто делегировать наборы наборов аргументов/опций для каждой части.
-
Внедрение в приложение какой-то более крупной системы, такой как RSpec, и я просто передал бы командную строку через свои параметры без моей обертки, зная их.
У меня все будет в порядке с некоторым параметром разделителя, например --
или --vmargs
в некоторых приложениях Java.
В мире Unix существует множество примеров реального мира (startx/X, git plumbing и фарфор), где один слой обрабатывает некоторые параметры, но распространяет остальные на нижний уровень.
Это, похоже, не работает. Каждый OptionParse.parse!
вызов будет выполнять исчерпывающую обработку, проваливаясь на что-либо, о чем он не знает. Наверное, я бы с удовольствием пропустил неизвестные варианты.
Любые намеки, возможно, альтернативные подходы приветствуются.
Ответы
Ответ 1
Предполагая, что порядок, в котором будут выполняться парсеры, хорошо определен, вы можете просто сохранить дополнительные параметры во временной глобальной переменной и запустить 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
без восклицательного знака, который не будет удалять ключи командной строки из массива $*
и убедитесь, что в обоих наборах нет одинаковых опций. Я бы посоветовал не модифицировать массив $*
вручную, так как это затрудняет понимание вашего кода, если вы смотрите только на вторую часть, но вы можете это сделать. В этом случае вам придется игнорировать недопустимые параметры:
begin
opts.parse
rescue OptionParser::InvalidOption
puts "Warning: Invalid option"
end
Второй метод на самом деле не работает, как указано в комментарии. Однако, если вам нужно изменить массив $*
, вы можете сделать это вместо этого:
tmp = Array.new
while($*.size > 0)
begin
opts.parse!
rescue OptionParser::InvalidOption => e
tmp.push(e.to_s.sub(/invalid option:\s+/,''))
end
end
tmp.each { |a| $*.push(a) }
Это больше, чем немного взломанный, но он должен делать то, что вы хотите.
Ответ 2
Для потомков вы можете сделать это с помощью метода 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
Мне нужно решение, которое бы не выбрасывало OptionParser::InvalidOption
когда-либо, и не могло найти элегантного решения среди текущих ответов. Этот патч обезьяны основан на одном из других ответов, но очищает его и делает его более похожим на текущую семантику 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
Ответ 4
У меня такая же проблема, и я нашел следующее решение:
options = ARGV.dup
remaining = []
while !options.empty?
begin
head = options.shift
remaining.concat(parser.parse([head]))
rescue OptionParser::InvalidOption
remaining << head
retry
end
end
Ответ 5
Другое решение, которое полагается на 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
Ответ 6
Мне тоже было нужно то же самое... мне потребовалось некоторое время, но относительно простой способ работал отлично в конце.
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 script.
Ответ 7
У меня возникла аналогичная проблема, когда я писал script, который обернул рубиновый камень, которому нужны его собственные параметры с переданными ему аргументами.
Появилось следующее решение, в котором поддерживает параметры с аргументами для обернутого инструмента. Он работает, анализируя его с помощью первого 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)
Вероятно, это не самый лучший или самый эффективный способ сделать это, но это сработало для меня.
Ответ 8
Я только что перешел с 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]
Тада: -)
Ответ 9
Моя попытка:
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(" Помощь "), а затем добавить собственный вариант справки
Ответ 10
Разбирайте параметры до первой неизвестной опции... блок может вызываться несколько раз, поэтому убедитесь, что это безопасно...
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}"