Может ли OptionParser пропустить неизвестные опции, которые будут позже обработаны в программе Ruby? - PullRequest
12 голосов
/ 04 сентября 2010

Есть ли способ запустить OptionParser несколько раз в одной программе Ruby, каждая с различными наборами параметров?

Например:

$ myscript.rb --subsys1opt a --subsys2opt b

Здесь myscript.rb будет использовать subsys1 и subsys2, делегируя им логику обработки опций, возможно, в последовательности, где сначала обрабатывается «a», а затем «b» в отдельном объекте OptionParser; каждый раз варианты выбора, относящиеся только к этому контексту. На последнем этапе можно проверить, что после обработки каждой детали ничего не осталось.

Варианты использования:

  1. В слабосвязанной интерфейсной программе, где различные компоненты имеют разные аргументы, я не хочу, чтобы «main» знал обо всем, просто делегировал наборы аргументов / опций для каждой части.

  2. Внедрение в мое приложение какой-нибудь более крупной системы, такой как RSpec, и я бы просто передавал командную строку через их параметры, чтобы моя оболочка не знала их.

Я бы тоже согласился с некоторыми опциями-разделителями, например -- или --vmargs в некоторых приложениях Java.

В мире Unix существует множество реальных примеров подобных вещей (startx / X, git plumbing и фарфор), где один слой обрабатывает некоторые параметры, а остальные распространяет на нижний уровень.

Из коробки это не похоже на работу. Каждый OptionParse.parse! вызов будет выполнять исчерпывающую обработку, не давая результатов, о которых он не знает. Думаю, я бы с удовольствием пропустил неизвестные варианты.

Любые советы, возможно, альтернативные подходы приветствуются.

Ответы [ 10 ]

5 голосов
/ 05 ноября 2016

Мне нужно решение, которое никогда не выкинет 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 голосов
/ 04 сентября 2010

Предполагая, что порядок запуска синтаксических анализаторов хорошо определен, вы можете просто сохранить дополнительные параметры во временной глобальной переменной и запустить 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) }

Это больше, чем просто хакерство, но оно должно делать то, что вы хотите.

3 голосов
/ 04 апреля 2013

Для потомков вы можете сделать это с помощью метода 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 останется с любым последующим --, будь то дополнительные параметры или просто обычные аргументы.

2 голосов
/ 19 ноября 2012

Другое решение, основанное на 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
2 голосов
/ 15 апреля 2011

У меня та же проблема, и я нашел следующее решение: <pre> options = ARGV.dup remaining = [] while !options.empty? begin head = options.shift remaining.concat(parser.parse([head])) rescue OptionParser::InvalidOption remaining << head retry end end

1 голос
/ 11 августа 2015

Мне тоже нужно было то же самое ... это заняло у меня некоторое время, но в конце концов относительно простой способ сработал.

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" будет содержать все аргументы командной строки без уже существующихразобрал в "опцию" хеш.Я передаю эти "аргументы" внешней программе, которая будет вызываться из этого скрипта ruby.

0 голосов
/ 28 января 2017

Разбор параметров до первого неизвестного параметра ... блок может вызываться несколько раз, поэтому убедитесь, что он безопасен ...

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}"
0 голосов
/ 13 декабря 2015

Моя попытка:

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 встроен в параметры, поэтому, если вы не хотите, чтобы справка не распознавалась в первый раз, вам нужно выполнить OptionParser :: Officious.delete ('help') 'перед созданием OptParser и затем добавить в него опция собственной справки

0 голосов
/ 03 июля 2014

Я только что перешел с 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]

Тада: -)

0 голосов
/ 30 мая 2014

Я столкнулся с подобной проблемой, когда писал сценарий, который заключал в себе рубиновый драгоценный камень, которому требовались свои собственные параметры с передаваемыми ему аргументами.

Я нашел следующее решение, в котором оно поддерживает параметры с аргументами для обернутого инструмента.Он работает, анализируя его через первый 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)

Возможно, не самый большой или самыйэффективный способ сделать это, но у меня это сработало.

...