在 Ruby 脚本中解析命令行参数

72
我希望从命令行调用一个Ruby脚本,并传递键/值对参数。
命令行调用:
$ ruby my_script.rb --first_name=donald --last_name=knuth

my_script.rb:

puts args.first_name + args.last_name
什么是标准的Ruby方式来做这件事?在其他语言中,我通常需要使用选项解析器。在Ruby中,我看到我们有ARGF.read,但是它似乎不能处理像这个例子中的键/值对。 OptionParser看起来很有前途,但我无法确定它是否支持这种情况。

https://www.ruby-toolbox.com/categories/CLI_Option_Parsers - Matheus Moreira
如果我没记错的话,Highline 似乎是用于请求用户输入的帮助函数。因此,我会使用 Highline 让我的控制台显示“名字:”,并等待他们的输入。在其中有特定的函数我应该寻找吗? - Don P
1
有很多宝石可供选择;该网站对库进行分类并按受欢迎程度排序。我甚至编写了自己的宝石,名为“acclaim”,它支持“--option=value”语法。虽然我没有时间维护我的免费软件项目。你应该选择一个得到更好支持的库。 - Matheus Moreira
这是一个关于选项解析器的好教程:http://www.dreamsyssoft.com/ruby-scripting-tutorial/optionparser-tutorial.php - Rocky Pulley
7个回答

135
Ruby内置的OptionParser可以很好地完成这项任务。将其与OpenStruct结合使用,你就可以轻松地完成任务:
require 'optparse'

options = {}
OptionParser.new do |opt|
  opt.on('--first_name FIRSTNAME') { |o| options[:first_name] = o }
  opt.on('--last_name LASTNAME') { |o| options[:last_name] = o }
end.parse!

puts options

options将包含参数和值的哈希表。

在命令行中不带参数保存并运行,结果如下:

$ ruby test.rb
{}

使用参数运行:

$ ruby test.rb --first_name=foo --last_name=bar
{:first_name=>"foo", :last_name=>"bar"}

这个例子使用哈希来包含选项,但你也可以使用OpenStruct,这将导致像你请求的那样使用:

require 'optparse'
require 'ostruct'

options = OpenStruct.new
OptionParser.new do |opt|
  opt.on('-f', '--first_name FIRSTNAME', 'The first name') { |o| options.first_name = o }
  opt.on('-l', '--last_name LASTNAME', 'The last name') { |o| options.last_name = o }
end.parse!

puts options.first_name + ' ' + options.last_name

$ ruby test.rb --first_name=foo --last_name=bar
foo bar

它甚至会自动创建你的-h--help选项:
$ ruby test.rb -h
Usage: test [options]
        --first_name FIRSTNAME
        --last_name LASTNAME

你也可以使用短标志:

require 'optparse'

options = {}
OptionParser.new do |opt|
  opt.on('-f', '--first_name FIRSTNAME') { |o| options[:first_name] = o }
  opt.on('-l', '--last_name LASTNAME') { |o| options[:last_name] = o }
end.parse!

puts options

将其运行完整:
$ ruby test.rb -h
Usage: test [options]
    -f, --first_name FIRSTNAME
    -l, --last_name LASTNAME
$ ruby test.rb -f foo --l bar
{:first_name=>"foo", :last_name=>"bar"}

您也可以轻松添加选项的内联解释:

OptionParser.new do |opt|
  opt.on('-f', '--first_name FIRSTNAME', 'The first name') { |o| options[:first_name] = o }
  opt.on('-l', '--last_name LASTNAME', 'The last name') { |o| options[:last_name] = o }
end.parse!

并且:

$ ruby test.rb -h
Usage: test [options]
    -f, --first_name FIRSTNAME       The first name
    -l, --last_name LASTNAME         The last name

OptionParser还支持将参数转换为类型,例如整数或数组。有关更多示例和信息,请参阅文档。
您还应查看右侧的相关问题列表:
- “Ruby中真的很便宜的命令行选项解析” - “通过命令行向Ruby脚本传递变量

这怎么不是最佳答案? - njh
我不知道。就像他们所说的,“因人而异”。 - the Tin Man

43

在@MartinCortez的答案基础上,这里提供了一个简短的代码,可以对键值对进行哈希处理,其中值必须用等号=连接。它还支持没有值的标志参数:

args = Hash[ ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/) ]

... 或者另外一种选择是...

args = Hash[ ARGV.flat_map{|s| s.scan(/--?([^=\s]+)(?:=(\S+))?/) } ]

传入命令 -x=foo -h --jim=jam,该函数返回结果为:{"x"=>"foo", "h"=>nil, "jim"=>"jam"},你可以使用以下方式:

puts args['jim'] if args.key?('h')
#=> jam

虽然有多个库可以处理这个问题,包括Ruby自带的GetoptLong,但我个人更喜欢自己编写。下面是我使用的模式,它使得代码比较通用,不会绑定到特定的使用格式,并且足够灵活,可以允许混合标志、选项和必需的参数以各种顺序出现:

USAGE = <<ENDUSAGE
Usage:
   docubot [-h] [-v] [create [-s shell] [-f]] directory [-w writer] [-o output_file] [-n] [-l log_file]
ENDUSAGE

HELP = <<ENDHELP
   -h, --help       Show this help.
   -v, --version    Show the version number (#{DocuBot::VERSION}).
   create           Create a starter directory filled with example files;
                    also copies the template for easy modification, if desired.
   -s, --shell      The shell to copy from.
                    Available shells: #{DocuBot::SHELLS.join(', ')}
   -f, --force      Force create over an existing directory,
                    deleting any existing files.
   -w, --writer     The output type to create [Defaults to 'chm']
                    Available writers: #{DocuBot::Writer::INSTALLED_WRITERS.join(', ')}
   -o, --output     The file or folder (depending on the writer) to create.
                    [Default value depends on the writer chosen.]
   -n, --nopreview  Disable automatic preview of .chm.
   -l, --logfile    Specify the filename to log to.

ENDHELP

ARGS = { :shell=>'default', :writer=>'chm' } # Setting default values
UNFLAGGED_ARGS = [ :directory ]              # Bare arguments (no flag)
next_arg = UNFLAGGED_ARGS.first
ARGV.each do |arg|
  case arg
    when '-h','--help'      then ARGS[:help]      = true
    when 'create'           then ARGS[:create]    = true
    when '-f','--force'     then ARGS[:force]     = true
    when '-n','--nopreview' then ARGS[:nopreview] = true
    when '-v','--version'   then ARGS[:version]   = true
    when '-s','--shell'     then next_arg = :shell
    when '-w','--writer'    then next_arg = :writer
    when '-o','--output'    then next_arg = :output
    when '-l','--logfile'   then next_arg = :logfile
    else
      if next_arg
        ARGS[next_arg] = arg
        UNFLAGGED_ARGS.delete( next_arg )
      end
      next_arg = UNFLAGGED_ARGS.first
  end
end

puts "DocuBot v#{DocuBot::VERSION}" if ARGS[:version]

if ARGS[:help] or !ARGS[:directory]
  puts USAGE unless ARGS[:version]
  puts HELP if ARGS[:help]
  exit
end

if ARGS[:logfile]
  $stdout.reopen( ARGS[:logfile], "w" )
  $stdout.sync = true
  $stderr.reopen( $stdout )
end

# etc.

1
这很好,但如果您还使用内置的ARGF通过文件名/标准输入读取流,则需要确保在使用ARGF.read之前消耗来自ARGV的参数,否则您将遇到“没有这样的文件或目录”错误。 - Lauren

10

Ruby中有许多命令行参数解析器:

个人而言,我会选择slopoptimist,它们不是标准的Ruby安装包中的一部分。

gem install slop

但它提供了简洁性和代码可读性。假设有稍微复杂一点的例子,其中包含必需的参数和默认值:

require 'slop'

begin
  opts = Slop.parse do |o|
    o.int '-a', '--age', 'Current age', default: 42
    o.string '-f', '--first_name', 'The first name', required: true
    o.string '-l', '--last_name', 'The last name', required: true
    o.bool '-v', '--verbose', 'verbose output', default: false
    o.on '-h','--help', 'print the help' do
      puts o
      exit
    end
  end

  p opts.to_hash
rescue Slop::Error => e
  puts e.message
end

optimisttrollop 的前身,使用起来非常简单,只需要很少的样板代码:

gem install optimist

require 'optimist'

opts = Optimist::options do
  opt :verbose, "verbose mode"
  opt :first_name, "The first name", type: :string, required: true
  opt :last_name, "The last name", type: :string, required: true
  opt :age, "Current age", default: 42
end

p opts

使用 OptionParser 的类似示例:

#!/usr/bin/env ruby

require 'optparse'
require 'ostruct'

begin
  options = OpenStruct.new
  OptionParser.new do |opt|
    opt.on('-a', '--age AGE', 'Current age') { |o| options.age = o }
    opt.on('-f', '--first_name FIRSTNAME', 'The first name') { |o| options.first_name = o }
    opt.on('-l', '--last_name LASTNAME', 'The last name') { |o| options.last_name = o }
    opt.on('-v', '--verbose', 'Verbose output') { |o| options.verbose = true }
  end.parse!

  options[:age] = 42 if options[:age].nil?
  raise OptionParser::MissingArgument.new('--first_name') if options[:first_name].nil?
  raise OptionParser::MissingArgument.new('--last_name') if options[:last_name].nil?
  options[:verbose] = false if options[:verbose].nil?

rescue OptionParser::ParseError => e
  puts e.message
  exit
end
< p >< code > GetoptLong 解析更为复杂:

require 'getoptlong'

opts = GetoptLong.new(
  [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
  [ '--first_name', '-f', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--last_name', '-l', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--age','-a', GetoptLong::OPTIONAL_ARGUMENT ],
  [ '--verbose','-v', GetoptLong::OPTIONAL_ARGUMENT ]
)
begin
  options = {}
  options[:verbose] = false
  options[:age] = 42
  opts.each do |opt, arg|
    case opt
    when '--help'
        puts <<-EOF
  usage: ./getlongopts.rb [options]

      -a, --age         Current age
      -f, --first_name  The first name
      -l, --last_name   The last name
      -v, --verbose     verbose output
      -h, --help        print the help

        EOF
    when '--first_name'
      options[:first_name] = arg
    when '--last_name'
      options[:last_name] = arg
    when '--age'
      options[:age] = arg.to_i
    when '--verbose'
      options[:verbose] = arg
    else
      puts "unknown option `#{opt}`"
      exit 1
    end
  end

  raise GetoptLong::MissingArgument.new('Missing argument --first_name') if options[:first_name].nil?
  raise GetoptLong::MissingArgument.new('Missing argument --last_name') if options[:last_name].nil?

rescue GetoptLong::Error => e
  puts e.message
  exit
end

puts options

命令行参数从来不是一项复杂的任务,把时间花在阅读/编写更有用的代码上吧 :)


这是一个很好的概述可用选择及其比较。值得更多的赞。 - Nadim Hussami

6
我个人使用 Docopt。这种方式更加清晰、可维护和易于阅读。
可以查看 Ruby 实现的 文档 以获取示例。使用方法非常简单。
gem install docopt

Ruby代码:

doc = <<DOCOPT
My program who says hello

Usage:
  #{__FILE__} --first_name=<first_name> --last_name=<last_name>
DOCOPT

begin
  args = Docopt::docopt(doc)
rescue Docopt::Exit => e
  puts e.message
  exit
end

print "Hello #{args['--first_name']} #{args['--last_name']}"

然后调用:
$ ./says_hello.rb --first_name=Homer --last_name=Simpsons
Hello Homer Simpsons

没有参数:

$ ./says_hello.rb
Usage:
  says_hello.rb --first_name=<first_name> --last_name=<last_name>

2
这是在Ruby中使用选项的唯一明智的答案选项是从您的帮助消息生成和解析的!太棒了。 - not2qubit

4

以下是 myscript.rb 文件中的标准 Ruby Regexp 代码:

args = {}

ARGV.each do |arg|
  match = /--(?<key>.*?)=(?<value>.*)/.match(arg)
  args[match[:key]] = match[:value] # e.g. args['first_name'] = 'donald'
end

puts args['first_name'] + ' ' + args['last_name']

在命令行上:

$ ruby script.rb --first_name=donald --last_name=knuth

生成:

$ donald knuth

1
这很不错!对我来说,使用=来消除-f foo-x -y bar之间的歧义要求有点不切实际,但这是一个不错的快速技巧。 - Phrogz
1
没错。我只是按照他上面的格式来做的。根据问题,我不确定他是否关心消歧。 - Marty Cortez
那看起来很棒,但你会如何定义sARGV.join.to_s - Marty Cortez
我再也看不到它了(你的评论) - Marty Cortez
是的,我删除了我的评论,因为正如你所指出的那样,它是错误的。 :) 现在它在我的答案中了。删除其他评论以进行清理。 - Phrogz

1
这里是对@Phrogz的回答进行了轻微修改:这个修改允许你传递一个带有空格的字符串。
args= Hash[ ARGV.join(' ').scan(/--?([^=\s]+)(?:="(.*?)"+)?/)]
在命令行中,可以像这样传递字符串:
ruby my_script.rb '--first="Boo Boo" --last="Bear"'
或者从另一个Ruby脚本中像这样传递:
system('ruby my_script.rb \'--first="Boo Boo" --last="Bear"\'')
结果:
{"first"=>"Boo Boo", "last"=>"Bear"}

1

一个改进版本,处理不是选项的参数,带参数的参数以及 -a--a

def parse(args)
  parsed = {}

  args.each do |arg|
    match = /^-?-(?<key>.*?)(=(?<value>.*)|)$/.match(arg)
    if match
      parsed[match[:key].to_sym] = match[:value]
    else
      parsed[:text] = "#{parsed[:text]} #{arg}".strip
    end
  end

  parsed
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接