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 文件名”?

编辑:

有一点我之前没有提到,因为直到特罗洛普的作者提到这个库适合“一个[800行]文件”,我才明白,我不仅在寻找干净的语法,而且在寻找一种具有以下特征的技术:

  1. 整个代码可以包含在脚本文件中(而不会淹没实际的脚本本身,实际的脚本本身可能只有几十行) ,这样就可以在使用标准 Ruby 1.8的任何系统的 bin目录中删除单个文件。[5-7]安装和使用它。如果您不能编写一个没有必要语句的 Ruby 脚本,并且解析几个选项的代码不超过十几行,那么您就不符合这个要求。

  2. 代码很小,也很简单,人们可以记住足够多的代码,直接输入代码就可以了,而不需要从其他地方剪切和粘贴。想象一下这样一种情况: 你在一个没有互联网接入的防火墙服务器的控制台上,你想为客户端编写一个快速的脚本。我不知道你是怎么想的,但是(除了没有达到上面的要求之外)即使是记住45行简化的 microoptparse 也不是我想做的事情。

55682 次浏览

你可以试试这样:

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

I totally understand why you want to avoid optparse - it can get too much. But there are a few far "lighter" solutions (compared to OptParse) that come as libraries but are simple enough to make a single gem installation worthwhile.

例如,查看 这个 OptiFlag 的例子。只需要几行代码就可以进行处理。一个适合你的情况的略微精简的例子:

require 'optiflag'


module Whatever extend OptiFlagSet
flag "f"
and_process!
end


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

还有很多定制的例子。我记得使用另一个更容易,但它逃脱了我现在,但我会回来,并添加一个注释在这里,如果我找到它。

I share your distaste for require 'getopts', mainly due to the awesomeness that is 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}

你有没有考虑过 Wycat 的 索尔?我觉得比 optparse 干净多了。如果您已经编写了一个脚本,那么可能需要更多的工作来格式化它或者为 thor 重构它,但是它确实使处理选项非常简单。

Here's the example snippet from the README:

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

雷神会自动映射如下命令:

app install myname --force

转换为:

MyApp.new.install("myname")
# with {'force' => true} as options hash
  1. 继承 Thor 以将类转换为选项映射器
  2. 将其他无效标识符映射到特定方法
  3. 下面描述方法。第一个参数是使用信息,第二个参数是描述。
  4. 提供任何附加选项。这些会从... 和-帕拉姆汇集起来。在本例中,添加了—— force 和 a-f 选项。

Trollop is pretty cheap.

作为 妓女的作者,我不能相信人们认为在期权解析器中是合理的东西。说真的。让人难以置信。

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

下面是特洛洛普版本的上面:

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,很容易拾取。

每个选项的最低运作成本,这很重要。

下面是我通常使用的标准技巧:

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

显然@William Morgan 和我想法一致。我昨晚刚刚在 Github 上发布了一个类似于 Trollop 的库(怎么命名的?)在 Github 上搜索 OptionParser 之后,请参阅 开关

虽然有一些不同,但是理念是一样的。一个明显的区别是,Switch 依赖于 OptionParser。

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

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

通过使用每个选项解析器来解决我遇到的问题。 您可以使用这些例子和我的总结来做出一个信息丰富的决策。 请随意向列表中添加更多实现。 :)

我在开发 我自己的选项解析器 gem 称为 Aclaim

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

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

目前还没有稳定的版本,但是我已经实现了一些特性,比如:

  • 自定义选项解析器
  • 允许最小值和可选项的选项参数的灵活解析
  • support for many option styles
  • 替换、附加或引发同一选项的多个实例
  • 自定义选项处理程序
  • custom type handlers
  • 公共标准库类的预定义处理程序

There's a lot of emphasis on commands so it might be a little heavy for simple command line parsing but it works well and I've been using it on all of my projects. If you're interested in the command interface aspect then check out 项目的 GitHub 页面 for more information and examples.

这就是我用来做非常非常便宜的 args 的原理:

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


main

所以如果你运行 programname foo bar,它会调用 foo 和 bar。

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

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

我创建了一个非常简单但很有用的解析器: 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

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

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

下面是保存为 ./test和 chmod 为 +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令牌 shebang; 你可能需要用到 Ruby 的实际路径(比如 /usr/local/bin/ruby -w)替换 /usr/bin/env ruby -w,或者从包装器脚本运行它,或者别的什么。

如果您想要一个简单的命令行解析器,用于不使用 gems 的键/值命令:

But this 只有 works if you always have key/value pairs.

# 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

下面是我最喜欢的快速而肮脏的选项解析器:

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

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

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

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 属性作为一个便宜的解析器是一个新颖的想法:

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

这是非常相似的接受答案,但使用 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

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

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() will return an array containing the keys for switches that are present

这是意味着允许一些快速和肮脏的东西,像..。

  • Validate I've got the right number of positional arguments regardless of the switches passed in ( arghash.argc == 2 )
  • Access positional arguments by their relative position, regardless of switches appearing before or interspersed with positional arguments ( e.g. arghash[1] always gets the second non-switch argument).
  • 支持命令行中的值分配开关,例如“—— max = 15”,arghash['--max=']可以访问这些开关,在给定示例命令行的情况下,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 = 的值,并在没有传递参数的情况下添加第4个位置参数。

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