Как указать обязательный переключатель (не аргумент) с Ruby OptionParser? - PullRequest
47 голосов
/ 09 октября 2009

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

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

Ответы [ 8 ]

92 голосов
/ 27 января 2010

Подход, использующий optparse, который обеспечивает дружественный вывод на отсутствующие коммутаторы:

#!/usr/bin/env ruby
require 'optparse'

options = {}

optparse = OptionParser.new do |opts|
  opts.on('-f', '--from SENDER', 'username of sender') do |sender|
    options[:from] = sender
  end

  opts.on('-t', '--to RECIPIENTS', 'comma separated list of recipients') do |recipients|
    options[:to] = recipients
  end

  options[:number_of_files] = 1
  opts.on('-n', '--num_files NUMBER', Integer, "number of files to send (default #{options[:number_of_files]})") do |number_of_files|
    options[:number_of_files] = number_of_files
  end

  opts.on('-h', '--help', 'Display this screen') do
    puts opts
    exit
  end
end

begin
  optparse.parse!
  mandatory = [:from, :to]                                         # Enforce the presence of
  missing = mandatory.select{ |param| options[param].nil? }        # the -t and -f switches
  unless missing.empty?                                            #
    raise OptionParser::MissingArgument.new(missing.join(', '))    #
  end                                                              #
rescue OptionParser::InvalidOption, OptionParser::MissingArgument      #
  puts $!.to_s                                                           # Friendly output when parsing fails
  puts optparse                                                          #
  exit                                                                   #
end                                                                      #

puts "Performing task with options: #{options.inspect}"

Работа без переключателей -t или -f показывает следующий вывод:

Missing options: from, to
Usage: test_script [options]
    -f, --from SENDER                username of sender
    -t, --to RECIPIENTS              comma separated list of recipients
    -n, --num_files NUMBER           number of files to send (default 1)
    -h, --help

Запуск метода parse в предложении begin / rescue позволяет дружественное форматирование при других сбоях, таких как отсутствующие аргументы или недопустимые значения переключателя, например, попробуйте передать строку для переключателя -n.

59 голосов
/ 09 октября 2009

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

Самый простой способ, вероятно, состоит в том, чтобы проанализировать параметры с использованием выбранной вами библиотеки синтаксического анализа параметров и затем вызвать исключение OptionParser :: MissingArgument, если значение host равно nil.

Следующий код иллюстрирует

#!/usr/bin/env ruby
require 'optparse'

options = {}

optparse = OptionParser.new do |opts|
  opts.on('-h', '--host HOSTNAME', "Mandatory Host Name") do |f|
    options[:host] = f
  end
end

optparse.parse!

#Now raise an exception if we have not found a host option
raise OptionParser::MissingArgument if options[:host].nil?


puts "Host = #{options[:host]}"

Запуск этого примера с командной строкой

./program -h somehost

простое отображение "Host = somehost"

При работе с отсутствующим -h и отсутствием имени файла выдается следующий вывод

./program:15: missing argument:  (OptionParser::MissingArgument)

При запуске с командной строкой ./program -h выдает

/usr/lib/ruby/1.8/optparse.rb:451:in `parse': missing argument: -h (OptionParser::MissingArgument)
  from /usr/lib/ruby/1.8/optparse.rb:1288:in `parse_in_order'
  from /usr/lib/ruby/1.8/optparse.rb:1247:in `catch'
  from /usr/lib/ruby/1.8/optparse.rb:1247:in `parse_in_order'
  from /usr/lib/ruby/1.8/optparse.rb:1241:in `order!'
  from /usr/lib/ruby/1.8/optparse.rb:1332:in `permute!'
  from /usr/lib/ruby/1.8/optparse.rb:1353:in `parse!'
  from ./program:13
10 голосов
/ 28 октября 2010

Я превратил это в драгоценный камень, который вы можете скачать и установить с rubygems.org:

gem install pickled_optparse

И вы можете получить обновленный исходный код проекта на github:
http://github.com/PicklePumpers/pickled_optparse

- Предыдущая информация -

Это действительно очень меня беспокоило, поэтому я исправил это и сохранил использование супер СУХОЙ.

Чтобы сделать обязательным переключение, просто добавьте: обязательный символ в любое место в массиве параметров, например:

opts.on("-f", "--foo [Bar]", String, :required, "Some required option") do |option|
  @options[:foo] = option
end

Затем в конце вашего блока OptionParser добавьте один из них, чтобы распечатать недостающие ключи и инструкции по использованию:

if opts.missing_switches?
  puts opts.missing_switches
  puts opts
  exit
end

И, наконец, чтобы все это заработало, вам нужно добавить куда-нибудь следующий файл "optparse_required_switches.rb" в ваш проект и потребовать его при анализе вашей командной строки.

Я написал небольшую статью с примером в своем блоге: http://picklepumpers.com/wordpress/?p=949

А вот модифицированный файл OptionParser с примером его использования:

required_switches_example.rb

#!/usr/bin/env ruby
require 'optparse'
require_relative 'optparse_required_switches'

# Configure options based on command line options
@options = {}
OptionParser.new do |opts|
  opts.banner = "Usage: test [options] in_file[.srt] out_file[.srt]"

  # Note that :required can be anywhere in the parameters

  # Also note that OptionParser is bugged and will only check 
  # for required parameters on the last option, not my bug.

  # required switch, required parameter
  opts.on("-s Short", String, :required, "a required switch with just a short") do |operation|
    @options[:operation] = operation
  end

  # required switch, optional parameter
  opts.on(:required, "--long [Long]", String, "a required switch with just a long") do |operation|
    @options[:operation] = operation
  end

  # required switch, required parameter
  opts.on("-b", "--both ShortAndLong", String, "a required switch with short and long", :required) do |operation|
    @options[:operation] = operation
  end

  # optional switch, optional parameter
  opts.on("-o", "--optional [Whatever]", String, "an optional switch with short and long") do |operation|
    @options[:operation] = operation
  end

  # Now we can see if there are any missing required 
  # switches so we can alert the user to what they 
  # missed and how to use the program properly.
  if opts.missing_switches?
    puts opts.missing_switches
    puts opts
    exit
  end

end.parse!

optparse_required_switches.rb

# Add required switches to OptionParser
class OptionParser

  # An array of messages describing the missing required switches
  attr_reader :missing_switches

  # Convenience method to test if we're missing any required switches
  def missing_switches?
    !@missing_switches.nil?
  end

  def make_switch(opts, block = nil)
    short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
    ldesc, sdesc, desc, arg = [], [], []
    default_style = Switch::NoArgument
    default_pattern = nil
    klass = nil
    n, q, a = nil

    # Check for required switches
    required = opts.delete(:required)

    opts.each do |o|

      # argument class
      next if search(:atype, o) do |pat, c|
        klass = notwice(o, klass, 'type')
        if not_style and not_style != Switch::NoArgument
          not_pattern, not_conv = pat, c
        else
          default_pattern, conv = pat, c
        end
      end

      # directly specified pattern(any object possible to match)
      if (!(String === o || Symbol === o)) and o.respond_to?(:match)
        pattern = notwice(o, pattern, 'pattern')
        if pattern.respond_to?(:convert)
          conv = pattern.method(:convert).to_proc
        else
          conv = SPLAT_PROC
        end
        next
      end

      # anything others
      case o
        when Proc, Method
          block = notwice(o, block, 'block')
        when Array, Hash
          case pattern
            when CompletingHash
            when nil
              pattern = CompletingHash.new
              conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)
            else
              raise ArgumentError, "argument pattern given twice"
          end
          o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
        when Module
          raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
        when *ArgumentStyle.keys
          style = notwice(ArgumentStyle[o], style, 'style')
        when /^--no-([^\[\]=\s]*)(.+)?/
          q, a = $1, $2
          o = notwice(a ? Object : TrueClass, klass, 'type')
          not_pattern, not_conv = search(:atype, o) unless not_style
          not_style = (not_style || default_style).guess(arg = a) if a
          default_style = Switch::NoArgument
          default_pattern, conv = search(:atype, FalseClass) unless default_pattern
          ldesc << "--no-#{q}"
          long << 'no-' + (q = q.downcase)
          nolong << q
        when /^--\[no-\]([^\[\]=\s]*)(.+)?/
          q, a = $1, $2
          o = notwice(a ? Object : TrueClass, klass, 'type')
          if a
            default_style = default_style.guess(arg = a)
            default_pattern, conv = search(:atype, o) unless default_pattern
          end
          ldesc << "--[no-]#{q}"
          long << (o = q.downcase)
          not_pattern, not_conv = search(:atype, FalseClass) unless not_style
          not_style = Switch::NoArgument
          nolong << 'no-' + o
        when /^--([^\[\]=\s]*)(.+)?/
          q, a = $1, $2
          if a
            o = notwice(NilClass, klass, 'type')
            default_style = default_style.guess(arg = a)
            default_pattern, conv = search(:atype, o) unless default_pattern
          end
          ldesc << "--#{q}"
          long << (o = q.downcase)
        when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
          q, a = $1, $2
          o = notwice(Object, klass, 'type')
          if a
            default_style = default_style.guess(arg = a)
            default_pattern, conv = search(:atype, o) unless default_pattern
          end
          sdesc << "-#{q}"
          short << Regexp.new(q)
        when /^-(.)(.+)?/
          q, a = $1, $2
          if a
            o = notwice(NilClass, klass, 'type')
            default_style = default_style.guess(arg = a)
            default_pattern, conv = search(:atype, o) unless default_pattern
          end
          sdesc << "-#{q}"
          short << q
        when /^=/
          style = notwice(default_style.guess(arg = o), style, 'style')
          default_pattern, conv = search(:atype, Object) unless default_pattern
        else
          desc.push(o)
      end

    end

    default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
    if !(short.empty? and long.empty?)
      s = (style || default_style).new(pattern || default_pattern, conv, sdesc, ldesc, arg, desc, block)
    elsif !block
      if style or pattern
        raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
      end
      s = desc
    else
      short << pattern
      s = (style || default_style).new(pattern, conv, nil, nil, arg, desc, block)
    end

    # Make sure required switches are given
    if required && !(default_argv.include?("-#{short[0]}") || default_argv.include?("--#{long[0]}"))
        @missing_switches ||= [] # Should be placed in initialize if incorporated into Ruby proper

        # This is more clear but ugly and long.
        #missing = "-#{short[0]}" if !short.empty?
        #missing = "#{missing} or " if !short.empty? && !long.empty?
        #missing = "#{missing}--#{long[0]}" if !long.empty?

        # This is less clear and uglier but shorter.
        missing = "#{"-#{short[0]}" if !short.empty?}#{" or " if !short.empty? && !long.empty?}#{"--#{long[0]}" if !long.empty?}"

        @missing_switches << "Missing switch: #{missing}"
    end

    return s, short, long,
      (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
      nolong
  end

end
6 голосов
/ 02 января 2016

Я придумал четкое и краткое решение, которое подводит итог ваших вкладов. Возникает исключение OptionParser::MissingArgument с отсутствующими аргументами в качестве сообщения. Это исключение перехватывается в блоке rescue вместе с остальными исключениями из OptionParser.

#!/usr/bin/env ruby
require 'optparse'

options = {}

optparse = OptionParser.new do |opts|
  opts.on('-h', '--host hostname', "Host name") do |host|
    options[:host] = host
  end
end

begin
  optparse.parse!
  mandatory = [:host]
  missing = mandatory.select{ |param| options[param].nil? }
  raise OptionParser::MissingArgument, missing.join(', ') unless missing.empty?
rescue OptionParser::ParseError => e
  puts e
  puts optparse
  exit
end

Запуск этого примера:

 ./program            
missing argument: host
Usage: program [options]
    -h, --host hostname              Host name
4 голосов
/ 23 декабря 2011

Если host требуется, то, конечно, это не опция , это аргумент .

Имея это в виду, вот способ решить вашу проблему. Вы можете опросить массив ARGV, чтобы узнать, был ли указан хост, и, если это не так, затем вызвать abort("You must specify a host!") или аналогичный, чтобы заставить вашу программу завершить работу с ошибкой.

2 голосов
/ 08 апреля 2013

Если вы делаете что-то вроде этого:

opts.on('-h', '--host',
          'required host name [STRING]') do |h|
    someoptions[:host] = h || nil
  end

Тогда someoptions[:host] будет либо значением из командной строки, либо nil (если вы не указали --host и / или не указали значение после --host), и вы можете легко его проверить (и условно сбой) после разбора:

fail "Hostname not provided" unless someoptions[:host]
1 голос
/ 14 июля 2016

Идея состоит в том, чтобы определить OptionParser, затем parse! это и puts это, если некоторые поля отсутствуют. Установка filename на пустую строку по умолчанию, вероятно, не лучший способ, но у вас есть идея.

require 'optparse'

filename = ''
options = OptionParser.new do |opts|
    opts.banner = "Usage: swift-code-style.rb [options]"

    opts.on("-iNAME", "--input-filename=NAME", "Input filename") do |name|
        filename = name
    end
    opts.on("-h", "--help", "Prints this help") do
        puts opts
        exit
    end
end

options.parse!

if filename == ''
    puts "Missing filename.\n---\n"
    puts options
    exit
end

puts "Processing '#{filename}'..."

Если -i filename отсутствует, отображается:

~/prj/gem/swift-code-kit ./swift-code-style.rb
Missing filename.
---
Usage: swift-code-style.rb [options]
    -i, --input-filename=NAME        Input filename
    -h, --help                       Prints this help
1 голос
/ 04 февраля 2010

Ответ от unknown (google) хороший, но содержит небольшую ошибку.

rescue OptionParser::InvalidArgument, OptionParser::MissingArgument

должно быть

OptionParser::InvalidOption, OptionParser::MissingArgument

В противном случае optparse.parse! вызовет стандартный вывод ошибки для OptionParser::InvalidOption, а не пользовательское сообщение.

...