繁体   English   中英

Ruby 中非常便宜的命令行选项解析

[英]Really Cheap Command-Line Option Parsing in Ruby

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

有时我想非常便宜地将一些命令行选项破解成一个简单的脚本。 一种有趣的方法,无需处理 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. 整个代码都可以包含在脚本文件中(而不会使实际脚本本身不堪重负,它可能只有几十行),这样就可以将单个文件放入任何具有标准 Ruby 的系统上的bin目录中1.8.【5-7】安装使用。 如果您无法编写没有 require 语句的 Ruby 脚本,并且解析几个选项的代码不到十多行,那么您就无法满足此要求。

  2. 代码足够小且简单,以至于人们可以记住足够多的内容以直接键入可以实现这一目的的代码,而不是从其他地方剪切和粘贴。 想一想您在没有 Internet 访问权限的防火墙服务器的控制台上的情况,并且您想将一个快速脚本放在一起供客户端使用。 我不了解您,但是(除了未能满足上述要求)即使是 45 行简化的 micro-optparse 也不是我愿意做的事情。

作为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。 这很重要。

我和你一样不喜欢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}

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

#!/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}")

既然似乎没有人提到它,而且标题确实指的是廉价的命令行解析,为什么不让 Ruby 解释器为您完成这项工作呢? 如果您传递-s开关(例如,在您的shebang 中),您将免费获得简单的开关,分配给单字母全局变量。 这是您使用该开关的示例:

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

这是当我将其另存为./test和 chmod it +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。 事实上,这个技术唯一让我烦恼的是在访问一个未设置的全局变量时你会得到一个警告(如果你启用了它们),但它仍然是错误的,所以它对于一次性工具来说工作得很好,而且速度很快脚本。

FelipeC 在“ 如何在 Ruby 中进行真正廉价的命令行选项解析”的评论中指出的一个警告是,您的 shell 可能不支持 3-token shebang; 您可能需要将/usr/bin/env ruby -w替换为您的 ruby​​ 的实际路径(如/usr/local/bin/ruby -w ),或者从包装脚本或其他方式运行它。

我构建了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

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

我通过使用每个选项解析器来比较几个选项解析器来解决我遇到的问题。 您可以使用这些示例和我的摘要来做出信息丰富的决定。 随意向列表中添加更多实现。 :)

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

例如,查看这个 OptiFlag 示例 只需几行即可处理。 根据您的情况稍微截断的示例:

require 'optiflag'

module Whatever extend OptiFlagSet
  flag "f"
  and_process!
end 

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

还有大量的定制示例 我记得使用了另一个更简单的方法,但它现在让我逃脱了,但如果我找到它,我会回来并在此处添加评论。

这就是我用于非常非常便宜的args:

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

main

因此,如果您运行programname foo bar它会先调用 foo,然后再调用 bar。 这对于一次性脚本很方便。

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

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

选项是正则表达式,因此“-h”也将匹配“--help”。

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

您可以尝试以下操作:

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

你有没有考虑过 wycats 的托尔 我认为它比 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. 提供任何其他选项。 这些将从 -- 和 - 参数编组。 在这种情况下,添加了 --force 和 -f 选项。

如果你想在不使用 gem 的情况下为键/值命令提供一个简单的命令行解析器:

但这在您始终拥有键/值对时才有效。

# 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

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

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 行代码片段,用于解析命令行并粘贴位置参数并切换到 Hash 对象(通常分配给我在下面的示例中命名为arghash的对象)。

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

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

这将成为这样的哈希。

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

除此之外,Hash 中还添加了两个方便的方法:

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

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

  • 无论传入的开关如何,验证我都获得了正确数量的位置参数( arghash.argc == 2
  • 通过它们的相对位置访问位置参数,不管开关出现在位置参数之前或穿插位置参数(例如arghash[1]总是获得第二个非开关参数)。
  • 支持命令行中的赋值开关,例如“--max=15”,它可以通过arghash['--max=']访问,在给定示例命令行的情况下产生值 '15'。
  • 使用非常简单的符号(例如arghash['-s']测试命令行中是否存在开关,如果它存在则计算为真,如果不存在则计算为 nil。
  • 使用设置操作测试是否存在开关或开关替代

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

  • 使用设置操作识别无效开关的使用,例如

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

  • 使用简单的Hash.merge()指定缺失参数的默认值,例如下面的示例,如果未设置 -max= 则填充一个值,如果未通过则添加第 4 个位置参数。

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

特罗洛普很便宜。

这与接受的答案非常相似,但使用ARGV.delete_if ,这是我在我的简单解析器中使用的 唯一真正的区别是带参数的选项必须在一起(例如-lfile )。

def usage
  "usage: #{File.basename($0)}: [-l<logfile>] [-q] file ..."
end

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

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

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

显然@WilliamMorgan 和我的想法很相似。 我昨晚刚刚向 Github 发布了在 Github 上搜索 OptionParser 后,我现在看到的是一个与 Trollop 类似的库(命名方式?),请参阅Switches

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

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...

我正在开发我自己的名为 Acclaim 的选项解析器 gem

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

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

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

  • 自定义选项解析器
  • 灵活解析选项的参数,允许最小和可选
  • 支持多种选项样式
  • 在同一选项的多个实例上替换、附加或引发
  • 自定义选项处理程序
  • 自定义类型处理程序
  • 通用标准库类的预定义处理程序

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

我创建了一个非常简单但有用的解析器: parseopt 它使用 Git 的内部选项解析器作为灵感,还有 Ruby 的 OptionParser。

它看起来像这样:

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

opts.on('b', 'bool', 'Boolean') do |v|
 $bool = v
end

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

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

opts.parse

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM