真正便宜的命令行选项在Ruby中解析

时间:2009-05-22 12:35:30

标签: ruby

编辑:请在回复之前阅读本文底部列出的两项要求。人们不断发布他们的新宝石和图书馆等等,这显然不符合要求。

有时我想非常便宜地将一些命令行选项破解成一个简单的脚本。一个有趣的方法,不用处理getopts或解析或类似的东西,是:

...
$quiet       = ARGV.delete('-d')
$interactive = ARGV.delete('-i')
...
# Deal with ARGV as usual here, maybe using ARGF or whatever.

这不是普通的Unix选项语法,因为它会接受选项非选项命令行参数,如“myprog -i foo bar -q”,但我可以忍受。 (有些人,比如Subversion的开发人员,更喜欢这个。有时我也会这样做。)

只是存在或不存在的选项不能比上述更简单地实现。 (一个赋值,一个函数调用,一个副作用。)是否有一种同样简单的方法来处理带参数的选项,例如“-f filename ”?

编辑:

我之前没有说过的一点,因为直到Trollop的作者提到图书馆适合“在一个[800行]文件中”之前我还没有弄清楚,我看起来不是仅用于干净的语法,但对于具有以下特征的技术:

  1. 整个代码可以包含在脚本文件中(不会压倒实际的脚本本身,可能只有几十行),这样就可以在{{1在任何具有标准Ruby 1.8。[5-7]的系统上安装并使用它。如果你不能编写一个没有require语句的Ruby脚本,并且解析几个选项的代码大概在十几行左右,你就不能满足这个要求。

  2. 代码很小而且非常简单,人们可以记住足够的代码来直接输入代码来完成这一操作,而不是从其他地方剪切和粘贴。想想你所在的防火墙服务器控制台没有互联网访问权限的情况,你想把一个快速的脚本拼凑起来供客户使用。我不了解你,但是(除了上面的要求失败之外)记住甚至45行简化的微光谱也不是我想做的事情。

20 个答案:

答案 0 :(得分:233)

作为Trollop的作者,我无法相信人们认为在选项解析器中合理的东西。认真。它令人难以置信。

为什么我必须创建一个扩展其他模块的模块来解析选项?为什么我必须继承任何东西?为什么我只需要订阅一些“框架”来解析命令行?

这是以上的Trollop版本:

opts = Trollop::options do
  opt :quiet, "Use minimal output", :short => 'q'
  opt :interactive, "Be interactive"
  opt :filename, "File to process", :type => String
end

就是这样。 opts现在是一个包含密钥:quiet:interactive:filename的哈希。你可以用它做任何你想做的事。你会得到一个漂亮的帮助页面,格式适合你的屏幕宽度,自动短参数名称,类型检查......你需要的一切。

这是一个文件,因此如果您不想要正式的依赖项,可以将它放在lib /目录中。它有一个很容易上手的最小DSL。

LOC每个选项的人。这很重要。

答案 1 :(得分:76)

我与require 'getopts'分享您的厌恶,主要是因为OptionParser的真棒:

% cat temp.rb                                                            
require 'optparse'
OptionParser.new do |o|
  o.on('-d') { |b| $quiet = b }
  o.on('-i') { |b| $interactive = b }
  o.on('-f FILENAME') { |filename| $filename = filename }
  o.on('-h') { puts o; exit }
  o.parse!
end
p :quiet => $quiet, :interactive => $interactive, :filename => $filename
% ruby temp.rb                                                           
{:interactive=>nil, :filename=>nil, :quiet=>nil}
% ruby temp.rb -h                                                        
Usage: temp [options]
    -d
    -i
    -f FILENAME
    -h
% ruby temp.rb -d                                                        
{:interactive=>nil, :filename=>nil, :quiet=>true}
% ruby temp.rb -i                                                        
{:interactive=>true, :filename=>nil, :quiet=>nil}
% ruby temp.rb -di                                                       
{:interactive=>true, :filename=>nil, :quiet=>true}
% ruby temp.rb -dif apelad                                               
{:interactive=>true, :filename=>"apelad", :quiet=>true}
% ruby temp.rb -f apelad -i                                              
{:interactive=>true, :filename=>"apelad", :quiet=>nil}

答案 2 :(得分:59)

这是我经常使用的标准技术:

#!/usr/bin/env ruby

def usage(s)
    $stderr.puts(s)
    $stderr.puts("Usage: #{File.basename($0)}: [-l <logfile] [-q] file ...")
    exit(2)
end

$quiet   = false
$logfile = nil

loop { case ARGV[0]
    when '-q' then  ARGV.shift; $quiet = true
    when '-l' then  ARGV.shift; $logfile = ARGV.shift
    when /^-/ then  usage("Unknown option: #{ARGV[0].inspect}")
    else break
end; }

# Program carries on here.
puts("quiet: #{$quiet} logfile: #{$logfile.inspect} args: #{ARGV.inspect}")

答案 3 :(得分:34)

由于没有人提及它,并且标题 引用廉价命令行解析,为什么不让红宝石翻译为你做这项工作呢?如果您通过-s开关(例如,在您的shebang中),您可以免费获得简单的开关,分配给单字母全局变量。以下是使用该开关的示例:

#!/usr/bin/env ruby -s
puts "#$0: Quiet=#$q Interactive=#$i, ARGV=#{ARGV.inspect}"

这是我将其保存为./test并将其保存为+x时的输出:

$ ./test
./test: Quiet= Interactive=, ARGV=[]
$ ./test -q foo
./test: Quiet=true Interactive=, ARGV=["foo"]
$ ./test -q -i foo bar baz
./test: Quiet=true Interactive=true, ARGV=["foo", "bar", "baz"]
$ ./test -q=very foo
./test: Quiet=very Interactive=, ARGV=["foo"]

有关详细信息,请参阅ruby -h。 :)

必须便宜得多。如果你尝试像-:这样的开关,它会引发一个NameError,所以那里有一些验证。当然,在非切换参数之后你不能有任何开关,但如果你需要一些花哨的东西,你真的应该在最小的OptionParser上使用。事实上,唯一令我烦恼的是这种技术,当你访问一个未设置的全局变量时,你会得到一个警告(如果你已经启用它们),但它仍然是假的,所以它适用于一次性工具和快速脚本。

一个警告(as pointed out by FelipeC in the comments)是你的shell可能不支持3-token shebang;您可能需要将/usr/bin/env ruby -w替换为ruby的实际路径(如/usr/local/bin/ruby -w),或者从包装脚本或其他内容运行它。

答案 4 :(得分:13)

我构建了micro-optparse来满足这个对一个简短但易于使用的选项解析器的明显需求。它的语法类似于Trollop,短70行。如果您不需要验证而且没有空行,则可以将其减少到45行。我认为这正是你所寻找的。

简短的例子:

options = Parser.new do |p|
  p.version = "fancy script version 1.0"
  p.option :verbose, "turn on verbose mode"
  p.option :number_of_chairs, "defines how many chairs are in the classroom", :default => 1
  p.option :room_number, "select room number", :default => 2, :value_in_set => [1,2,3,4]
end.process!

使用-h--help调用脚本将打印

Usage: micro-optparse-example [options]
    -v, --[no-]verbose               turn on verbose mode
    -n, --number-of-chairs 1         defines how many chairs are in the classroom
    -r, --room-number 2              select room number
    -h, --help                       Show this message
    -V, --version                    Print version

它检查输入是否与默认值相同,生成短访问者和长访问者,如果给出无效参数则打印描述性错误消息等。

compared several option-parser通过使用每个选项解析器来解决我遇到的问题。 您可以使用这些示例和我的摘要做出信息性决策。 随意添加更多实现到列表中。 :)

答案 5 :(得分:8)

我完全理解为什么你要避免使用optparse - 它可能会得到太多。但是有一些远远低于“轻量级”的解决方案(与OptParse相比)作为库而已,但它足够简单,可以使单个宝石安装变得有价值。

例如,请查看this OptiFlag example。只需几行处理。根据您的情况量身定制的略微截断的示例:

require 'optiflag'

module Whatever extend OptiFlagSet
  flag "f"
  and_process!
end 

ARGV.flags.f # => .. whatever ..

tons of customized examples too。我记得使用另一种更容易的东西,但它现在已经逃脱了,但如果我找到它,我会回来在这里添加评论。

答案 6 :(得分:4)

这是我用于真正,非常便宜的args:

def main
  ARGV.each { |a| eval a }
end

main

所以如果你运行programname foo bar,它会调用foo然后调用bar。这对于一次性脚本来说非常方便。

答案 7 :(得分:3)

您可以尝试以下内容:

if( ARGV.include( '-f' ) )
  file = ARGV[ARGV.indexof( '-f' ) + 1 )]
  ARGV.delete('-f')
  ARGV.delete(file)
end

答案 8 :(得分:3)

您是否考虑过wycats Thor?我认为它比optparse更清洁。如果您已经编写了一个脚本,那么可能需要更多的工作来格式化它或者将其重构为thor,但它确实使处理选项变得非常简单。

以下是自述文件中的示例代码段:

class MyApp < Thor                                                # [1]
  map "-L" => :list                                               # [2]

  desc "install APP_NAME", "install one of the available apps"    # [3]
  method_options :force => :boolean, :alias => :optional          # [4]
  def install(name)
    user_alias = options[:alias]
    if options.force?
      # do something
    end
    # ... other code ...
  end

  desc "list [SEARCH]", "list all of the available apps, limited by SEARCH"
  def list(search = "")
    # list everything
  end
end

Thor会自动映射命令:

app install myname --force

转换为:

MyApp.new.install("myname")
# with {'force' => true} as options hash
  1. 继承Thor以将类转换为选项映射器
  2. 将其他无效标识符映射到特定方法。在这种情况下,将-L转换为:list
  3. 请在下面描述方法。第一个参数是使用信息,第二个参数是描述。
  4. 提供任何其他选项。这些将由 - 和 - params编组。在这种情况下,会添加--force和-f选项。

答案 9 :(得分:3)

这是我最喜欢的快速选项解析器:

case ARGV.join
when /-h/
  puts "help message"
  exit
when /-opt1/
  puts "running opt1"
end

选项是正则表达式,所以&#34; -h&#34;也会匹配&#34; - help&#34;。

可读,易记,无外部库和最少的代码。

答案 10 :(得分:2)

Trollop相当便宜。

答案 11 :(得分:2)

如果你想要一个简单的命令行解析器来获取key / value命令而不使用gems:

但如果你总是拥有键/值对,那么只有才有效。

# example
# script.rb -u username -p mypass

# check if there are even set of params given
if ARGV.count.odd? 
    puts 'invalid number of arguments'
    exit 1
end

# holds key/value pair of cl params {key1 => value1, key2 => valye2, ...}
opts = {} 

(ARGV.count/2).times do |i|
    k,v = ARGV.shift(2)
    opts[k] = v # create k/v pair
end

# set defaults if no params are given
opts['-u'] ||= 'root'

# example use of opts
puts "username:#{opts['-u']} password:#{opts['-p']}"

如果您不需要检查,则可以使用:

opts = {} 

(ARGV.count/2).times do |i|
    k,v = ARGV.shift(2)
    opts[k] = v # create k/v pair
end

答案 12 :(得分:2)

这是我大多数脚本顶部使用的代码段:

arghash = Hash.new.tap { |h| # Parse ARGV into a hash
    i = -1                      
    ARGV.map{  |s| /(-[a-zA-Z_-])?([^=]+)?(=)?(.+)?/m.match(s).to_a }
     .each{ |(_,a,b,c,d)| h[ a ? "#{a}#{b}#{c}" : (i+=1) ] =
                             (a ? (c ? "#{d}" : true) : "#{b}#{c}#{d}") 
          }
    [[:argc,Proc.new  {|| h.count{|(k,_)| !k.is_a?(String)}}],
     [:switches, Proc.new {|| h.keys.select{|k| k[0] == '-' }}]
    ].each{|(n,p)| h.define_singleton_method(n,&p) }
}

我也讨厌在快捷脚本中要求其他文件。我的解决方案几乎是您的要求。我将10行代码片段粘贴到我的任何脚本的顶部,这些脚本将分析命令行并将位置args粘贴并切换到Hash对象(通常分配给我命名为 arghash 的对象)在以下示例中)。

这是您可能想解析的示例命令行...

  

./myexampleprog.rb -s -x=15 --longswitch arg1 --longswitch2=val1 arg2

将成为这样的哈希。

 { 
   '-s' => true, 
   '-x=' => '15', 
   '--longswitch' => true, 
   '--longswitch2=' => 'val1', 
   0 => 'arg1', 
   1 => 'arg2'
 }

除此之外,还为哈希添加了两种便捷方法:

  • argc()将返回非切换参数的计数。
  • switches()将返回一个数组,其中包含存在的开关的键

这意味着允许一些快速而肮脏的东西,例如...

  • 验证我有正确数量的位置参数,而不管传入(arghash.argc == 2)的切换如何
  • 通过位置的相对位置访问位置参数,而不考虑位置参数之前或位置之间是否出现开关(例如arghash[1]始终获得第二个非开关参数)。
  • 在命令行中支持值分配的开关,例如“ --max = 15”,可以由arghash['--max=']访问,给定示例命令行,该值将产生值“ 15”。
  • 使用诸如arghash['-s']之类的非常简单的符号测试命令行中是否存在开关。
  • 如果存在,则评估为true,否则为nil。
  • 使用

    之类的设置操作测试是否存在开关或其他开关

    puts USAGETEXT if !(%w(-h --help) & arghash.switches()).empty?

  • 通过设置操作(例如

    )来识别无效开关的使用

    puts "Invalid switch found!" if !(arghash.switches - %w(-valid1 -valid2)).empty?

  • 使用简单的Hash.merge()指定缺少参数的默认值,例如以下示例,如果未设置-max =,则填充-max =的值;如果未传递,则添加第4个位置参数

    with_defaults = {'-max=' => 20, 3 => 'default.txt'}.merge(arghash)

答案 13 :(得分:1)

这与接受的答案非常相似,但使用-l=file这是我在simple parser中使用的内容。唯一真正的区别是带参数的选项必须在一起(例如def usage "usage: #{File.basename($0)}: [-l=<logfile>] [-q] file ..." end $quiet = false $logfile = nil ARGV.delete_if do |cur| next false if cur[0] != '-' case cur when '-q' $quiet = true when /^-l=(.+)$/ $logfile = $1 else $stderr.puts "Unknown option: #{cur}" $stderr.puts usage exit 1 end end puts "quiet: #{$quiet} logfile: #{$logfile.inspect} args: #{ARGV.inspect}" )。

#!/opt/local/bin/python3.6                                                                                                                     

import sys, subprocess

class child_proc:
    def __init__ (self, useShell):
        print ("useShell:", useShell)
        acmd = ["/tmp/a.pl", "Hello", "World"]
        if useShell:
            proc = subprocess.Popen(acmd, stdout=subprocess.PIPE, shell=True)
        else:
            proc = subprocess.Popen(acmd, stdout=subprocess.PIPE)
        while True:
            line = proc.stdout.readline()
            if line:
                try:
                    aline = line.decode().rstrip()
                    print (aline)
                except UnicodeDecodeError as e:
                    print("Error decoding child output:", line, e)
                    break
            else:
                break

child_proc(len(sys.argv) > 1)

答案 14 :(得分:0)

显然@WilliamMorgan和我一样。我刚刚在Github上发布了我刚刚发布的内容,我在Github上搜索了OptionParser后,看到了与Trollop类似的库(命名如何?),请参阅Switches

有一些差异,但哲学是一样的。一个明显的区别是Switch依赖于OptionParser。

答案 15 :(得分:0)

我正在开发my own option parser gem called Acclaim

我编写它是因为我想创建git风格的命令行界面,并且能够将每个命令的功能干净地分成单独的类,但它也可以在没有整个命令框架的情况下使用:

(options = []) << Acclaim::Option.new(:verbose, '-v', '--verbose')
values = Acclaim::Option::Parser.new(ARGV, options).parse!
puts 'Verbose.' if values.verbose?

目前还没有稳定发布,但我已经实现了一些功能,如:

  • 自定义选项解析器
  • 灵活解析选项的参数,允许最小和可选
  • 支持多种选项样式
  • 替换,追加或加注同一选项的多个实例
  • 自定义选项处理程序
  • 自定义类型处理程序
  • 公共标准库类的预定义处理程序

有很多强调命令,所以对于简单的命令行解析可能有点重,但它运行良好,我一直在我的所有项目中使用它。如果您对命令界面方面感兴趣,请查看the project's GitHub page以获取更多信息和示例。

答案 16 :(得分:0)

假设一个命令最多只有一个动作和任意数量的选项,如下所示:

cmd.rb
cmd.rb action
cmd.rb action -a -b ...
cmd.rb action -ab ...

没有验证的解析可能是这样的:

ACTION = ARGV.shift
OPTIONS = ARGV.join.tr('-', '')

if ACTION == '***'
  ...
  if OPTIONS.include? '*'
    ...
  end
  ...
end

答案 17 :(得分:0)

https://github.com/soveran/clap

other_args = Clap.run ARGV,
  "-s" => lambda { |s| switch = s },
  "-o" => lambda { other = true }

46LOC(1.0.0),不依赖于外部选项解析器。完成工作。可能不像其他人那样功能齐全,但它是46LOC。

如果你检查代码,你可以很容易地复制基础技术 - 分配lambdas并使用arity来确保如果你真的不想要一个外部库,那么args的正确数量会跟着标志。

简单。便宜。


编辑:基础概念已经失效,因为我认为您可以将其复制/粘贴到脚本中以制作合理的命令行解析器。它肯定我会承诺记忆,但使用lambda arity作为一个廉价的解析器是一个新颖的想法:

flag = false
option = nil
opts = {
  "--flag" => ->() { flag = true },
  "--option" => ->(v) { option = v }
}

argv = ARGV
args = []

while argv.any?
  item = argv.shift
  flag = opts[item]

  if flag
    raise ArgumentError if argv.size < arity
    flag.call(*argv.shift(arity))
  else
    args << item
  end
end

# ...do stuff...

答案 18 :(得分:-1)

我将分享我自己一直在研究的简单选项解析器。它只有74行代码,它完成了Git内部选项解析器的基础知识。我把OptionParser作为灵感,也是Git的。

https://gist.github.com/felipec/6772110

看起来像这样:

opts = ParseOpt.new
opts.usage = "git foo"

opts.on("b", "bool", help: "Boolean") do |v|
 $bool = v
end

opts.on("s", "string", help: "String") do |v|
 $str = v
end

opts.on("n", "number", help: "Number") do |v|
 $num = v.to_i
end

opts.parse

答案 19 :(得分:-1)

EasyOptions根本不需要任何解析代码的选项。只需编写帮助文本,require,done。

## Options:
##   -i, --interactive  Interactive mode
##   -q, --quiet        Silent mode

require 'easyoptions'
unless EasyOptions.options[:quiet]
    puts 'Interactive mode enabled' if EasyOptions.options[:interactive]
    EasyOptions.arguments.each { |item| puts "Argument: #{item}" }
end