除了 Rails 之外,Ruby 能做的太多太多了,除了用于 Rails 开发之外,Ruby 用的最多的就是写各种 Command Line 工具来解决各种小问题,Command Line 工具又称为命令行工具。
提到用 Ruby 写命令行工具,就绕不过一个问题,如何解析命令行参数?
先啰嗦一下 Unix下的命令行工具,Unix 的命令行工具历史悠久,这里面故事非常非常多(以后再讲,或者参见 Unix编程艺术)。随着时间的推移,对于如何正确构建优良的命令行工具,Unix 社区慢慢形成了一整套完整的 Convertion 以及惯用法,如果你的命令行工具遵从这些 Convertion,那么用户将会非常容易的去使用你的命令行工具,甚至通过简洁的方式,将你的命令行工具和各种其他工具组合起来,用来完成各种复杂的操作。
正确的处理命令行参数对于写出高质量的命令行工具非常重要,那么如何正确的处理命令行参数呢?如果有 C 语言编程经验,或者用 C 语言写过命令行工具的人可能很熟悉getopt(GNU getopt_long()),getopt 是 C Library 中一个专门用于解析命令行参数的工具,通常用 C 去写命令行工具的时候,getop 是一个很自然选择。
当使用 Ruby 写命令行工具的时候,我们在不借助任何内置/外置的命令行参数解析工具的情况下,可以直接从 ARGV
取到传入命令行的参数,然后手工判断,验证并执行后续操作。不过从遵循Unix的命令行工具的Convertion角度来讲,我不建议你直接从 ARGV 取数值,而是利用现有的库来作这件事情。Ruby 的标准库内置提供了一个 getopt 的 Ruby实现 GetoptLong,GetoptLong基本上模拟了 C 语言版本的全部接口/功能,不过 Ruby 开发社区不推荐你使用 GetoptLong,而是建议使用另外一个也是内置的且更加强大的解析库:OptionParser。
这个世界上总是有人不断的重新发明轮子,除了 Ruby 已经内置的 OptionParser
,还有下面这些第三方实现的轮子:
Thor 是 Rails 3 以后内建的命令行工具,严格意义上说,Thor 不仅仅用于解析命令行参数,而是用于替代 rake 作为新的 task 标准工具,Thor 的命令行参数解能是自己实现的,我个人建议在写 Rails 的 task 的时候,把 Thor 作为首选,但是作一般用途的命令行工具,Thor有点 overkill 了。
Gli 是一个用于建立“Git-Like Interface Command Line Parser”的工具,这里我简单给出一个什么是“Git-Like”的解释。通常 Unix 下的命令行工具都符合一个哲学,即“作一件事并且把它做好”,但是有些功能强大复杂的工具,如 Git,可以通过指定不同的 Action
执行不同的操作,比如 git
的 push
和 pull
操作:
$ git push $ git pull
就是两个完全不同的操作,但是他们的command
部分都是git
,只是action
部分不同。我们也可以把这样的通过不同的action
来实现不同的操作的命令行工具叫做Command-Suit工具,即从功能上看,它不是一个命令,而是一个命令的suit
集合。Gli就是帮助你快速实现这种Command-Suit
的框架,如果你需要编写复杂的命令行工具,Gli是一个不错的选择。
Trollop
,Choice
和Optiflag
都是命令行参数的Ruby Parser,他们的目的一致,而且他们解析过程都遵循Unix的约定,只是实现各有不同,用法也不同,不过对我来说,他们都是一回事。就Unix命令行来说,参数只有Options,Arguments,以及Actions而已,所以具体用哪个,看你的个人喜好,简单对比下来我认为Choice
的DSL语法最易读,简洁,优雅,如果你需要这些第三方Command Line parser的时候,不妨考虑一下Choice。不过我奉行另外一个原则,如果系统内置了的,我就不考虑第三方gem,而且Ruby内置的OptionParser足够强大,能满足我对解析Unix的命令行参数的一切需求,所以我优选使用OptionParser。这里我简单猜测一下为什么还有这么多第三方的轮子,第一是不知道Ruby已经内置了这个,第二个可能就是不爽Ruby内置的这个parser的文档或用法,虽然OptionParser足够强大灵活,但是不代表它好用,容易上手,相反,它的文档就相当坑爹!
下面这张图就是Ruby给出的OptionParser的文档,除了这张图片之外就是一个官方范例,然后就没了… 说实话我第一眼看了这张图和官方范例后感觉看不懂,需要反复通过Google各种文章和范例,才了解到了OptionParser的基本用法。
+--------------+ | OptionParser |<>-----+ +--------------+ | +--------+ | ,-| Switch | on_head -------->+---------------+ / +--------+ accept/reject -->| List |<|>- | |<|>- +----------+ on ------------->+---------------+ `-| argument | : : | class | +---------------+ |==========| on_tail -------->| | |pattern | +---------------+ |----------| OptionParser.accept ->| DefaultList | |converter | reject |(shared between| +----------+ | all instances)| +---------------+
通常的Unix命令行参数包含下面这些形式:
short option
或者long option
。Option的类型有两种,switch
或flag
,switch
不带argument,而flag
带有argument。git
命令的push
或者pull
等等。举个例子git log --max-count=10
:git
是command。log
是action,表示查看git的提交历史。--max-count
就是option,表示最多显示N条commit记录。而最后的=10
就是argument,表示option的数值,即查看最后10条历史提交记录。所有的Unix命令行工具都遵循这样的一个约定,这里需要主意一下,Argument前面的=
在很多命令行工具中是可以省略的。
用 OptionParser
创建一个简单的命令行工具,通常我们只需要创建一个OptionParser
的实例 instance,然后给这个 instance 传入一个block,在这个 block 内部我们就可以使用 OptionParser 提供的方法来解析命令行参数,特别是用 on
方法来根据定义捕捉各种参数,并将参数解析成可被使用的 Ruby 数据,如 String,Boolean,Array 以及 Hash 等。而 on
方法最让人困惑的地方就是它异常灵活参数处理,比如on
方法的第一个参数,如果是一个-
加一个非空格字符,则把这个参数当作 switch 来处理,例如 on('-n')
,如果是一个-
开头的字符,后面跟着一个空格外加另外一个字符,那么就把这个参数当作flag处理,例如on('-n NAME')
。如果on
方法的参数超过两个,并且两个都是String,那么则视这两个参数表示一个意思,例如 on('-n NAME', '--name NAME')
。如此这般的例子还有很多,如果有更高需求的朋友,我建议你还是直接去啃源代码吧。
下面我创建一个名为 my_awesome_command.rb
的命令行工具,这个工具直接输出我的命令行参数解析的结果,我用中文注释来说明OptionParser 是怎么用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #!/usr/bin/env ruby require 'optparse' options = {} option_parser = OptionParser.new do |opts| # 这里是这个命令行工具的帮助信息 opts.banner = 'here is help messages of the command line tool.' # Option 作为switch,不带argument,用于将 switch 设置成 true 或 false options[:switch] = false # 下面第一项是 Short option(没有可以直接在引号间留空),第二项是 Long option,第三项是对 Option 的描述 opts.on('-s', '--switch', 'Set options as switch') do # 这个部分就是使用这个Option后执行的代码 options[:switch] = true end # Option 作为 flag,带argument,用于将argument作为数值解析,比如"name"信息 #下面的“value”就是用户使用时输入的argument opts.on('-n NAME', '--name Name', 'Pass-in single name') do |value| options[:name] = value end # Option 作为 flag,带一组用逗号分割的arguments,用于将arguments作为数组解析 opts.on('-a A,B', '--array A,B', Array, 'List of arguments') do |value| options[:array] = value end end.parse! puts options.inspect |
执行结果
$ ruby my_awesome_command.rb -h here is help messages of the command line tool. -s, --switch Set options as switch -n, --name Name Pass-in single name -a, --array A,B List of arguments $ ruby my_awesome_command.rb -s {:switch=>true} $ ruby my_awesome_command.rb -n Daniel {:switch=>false, :name=>"Daniel"} $ ruby my_awesome_command.rb -a Foo,Bar {:switch=>false, :array=>["Foo", "Bar"]}
补充2点说明
希望以上内容能够帮助你掌握写出符合Unix标准的命令行参数的工具,如果要写出易用,对用户友好,跟其他命令行工具互动良好,可测试,可维护,可格式化输出内容的真正awesome
的命令行工具,您仍然需要继续努力,加油吧!