在Ruby中真正廉价的命令行选项parsing

编辑:请回答之前, 阅读本文底部列出的两个要求。 人们不断发布新的gem和图书馆,而这些显然不符合要求。

有时候我想非常便宜地把一些命令行选项变成一个简单的脚本。 一个有趣的方式来做到这一点,而不涉及getopts或parsing或类似的东西,是:

... $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 1.8。[5-7]安装和使用它。 如果你不能编写一个没有require语句的Ruby脚本,并且parsing一些选项的代码在十几行之内,你就不符合这个要求。

  2. 代码非常小巧,足够简单,人们可以记住足够多的代码来直接input代码,而不是从其他地方剪切和粘贴。 考虑一下你在没有互联网接入的防火墙服务器的控制台上的情况,并且想要把一个快速的脚本放在一起以供客户端使用。 我不了解你,但是(除了上面的要求之外)记住即使是45行简化的微型光栅也不是我所关心的。

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

为什么我必须创build一个扩展其他模块来parsing选项的模块? 为什么我需要inheritance任何东西? 为什么我不得不订阅一些“框架”来parsing命令行呢?

这是上面的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 hash, :interactive:filename 。 你可以随心所欲地做任何事情。 你会得到一个美丽的帮助页面,格式化为适合你的屏幕宽度,自动短参数名称,types检查…你需要的一切。

这是一个文件,所以如果你不想要正式的依赖,你可以把它放在你的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}") 

由于没有人提到它,标题确实指的是廉价的命令行parsing,为什么不只是让ruby解释器为你做的工作? 如果你通过-s开关(例如你的shebang),你可以免费得到污垢简单的开关,分配给单个字母的全局variables。 这是你使用该开关的例子:

 #!/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错误,所以这里有一些validation。 当然,在非转换参数之后,你不能有任何开关,但是如果你需要某些东西,你应该至less使用OptionParser。 实际上,唯一让我恼火的是这个技巧是当你访问一个未设置的全局variables时,你会得到一个警告(如果你已经启用了它们),但是它仍然是错误的,所以它适用于一次性工具,脚本。

我build立了微型optparse来填补这个显而易见的需要一个简短的,但易于使用的选项分析器。 它有一个类似Trollop的语法,是70行短。 如果你不需要validation,可以没有空行,你可以把它削减到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 

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

我通过使用每个选项parsing器来比较几个选项parsing器的问题。 你可以使用这些例子和我的总结作出翔实的决定。 随意添加更多的实现到列表中。 🙂

我完全理解你为什么想要避免select – 它可以得到太多。 但是,有一些远比“更轻”的解决scheme(相比于OptParse)来得像库,但是足够简单,使得一个gem安装值得。

例如,看看这个OptiFlag的例子 。 只是几行处理。 根据你的情况量身定做一个稍微截断的例子:

 require 'optiflag' module Whatever extend OptiFlagSet flag "f" and_process! end ARGV.flags.f # => .. whatever .. 

还有很多定制的例子 。 我记得使用另一个更容易,但它现在已经逃脱了,但我会回来,如果我find了这里添加评论。

Trollop相当便宜。

这是我真正使用的,真正便宜的参数:

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

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

你可以尝试像这样:

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

你有没有考虑过威尔士的托尔 ? 我认为它比optparse更清洁。 如果你已经写了一个脚本,格式化它或重构它可能是更多的工作,但它确实使处理选项非常简单。

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

 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. 从Thorinheritance来把一个类变成一个选项映射器
  2. 将附加的无效标识符映射到特定的方法。 在这种情况下,将-L转换为:列表
  3. 请描述下面的方法。 第一个参数是使用信息,第二个参数是描述。
  4. 提供任何其他选项。 这些将被从 – 和 – params。 在这种情况下,会添加–force和-f选项。

这是我最喜欢的快捷selectparsing器:

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

选项是正则expression式,所以“-h”也会匹配“–help”。

易读,易于记忆,无需外部库和最less的代码。

如果你想要一个简单的命令行parsing器的键/值命令,而不使用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 

https://github.com/soveran/clap

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

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

如果您检查代码,您可以很容易地复制底层技术 – 分配lambdaexpression式并使用arity来确保适当数量的参数跟随标志,如果您真的不需要外部库。

简单。 低廉。

显然@WilliamMorgan和我想的一样。 我刚刚发布了Github,现在我在Github上search了OptionParser之后,看到了一个与Trollop类似的库(命名方式),请参阅开关

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

我正在开发我自己的选项parsing器gemAcclaim 。

我写了它,因为我想创buildgit风格的命令行界面,并且能够将每个命令的function分离成不同的类,但是也可以在没有整个命令框架的情况下使用:

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

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

  • 自定义选项parsing器
  • 灵活的parsing选项的参数,允许最小值和可选值
  • 支持多种选项样式
  • replace,附加或提高同一选项的多个实例
  • 自定义选项处理程序
  • 自定义types处理程序
  • 预定义的公用标准库类的处理程序

有很多强调命令,所以它可能有点沉重的简单的命令行parsing,但它运作良好,我一直在我的所有项目中使用它。 如果您对命令接口方面感兴趣,请查看项目的GitHub页面以获取更多信息和示例。

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

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

没有validation的parsing可能是这样的:

 ACTION = ARGV.shift OPTIONS = ARGV.join.tr('-', '') if ACTION == '***' ... if OPTIONS.include? '*' ... end ... end 

我将分享我自己的一些简单的选项parsing器,我一直在努力。 这仅仅是74行代码,它是Git内部选项parsing器所做的基础知识。 我以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 

EasyOptions根本不需要任何选项parsing代码。 只需编写帮助文本,要求完成。

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