用 OptionParser 构建 Command Line 工具

17 条评论 , 8 次修正,16684 次阅读,最后更新于 2014年07月04日

Ruby 除了 Rails 还能做什么?

除了 Rails 之外,Ruby 能做的太多太多了,除了用于 Rails 开发之外,Ruby 用的最多的就是写各种 Command Line 工具来解决各种小问题,Command Line 工具又称为命令行工具。

提到用 Ruby 写命令行工具,就绕不过一个问题,如何解析命令行参数?

Unix 下的命令行工具

先啰嗦一下 Unix 下的命令行工具,Unix 的命令行工具历史悠久,这里面故事非常非常多(以后再讲,或者参见 Unix 编程艺术)。随着时间的推移,对于如何正确构建优良的命令行工具,Unix 社区慢慢形成了一整套完整的 Convertion 以及惯用法,如果你的命令行工具遵从这些 Convertion,那么用户将会非常容易的去使用你的命令行工具,甚至通过简洁的方式,将你的命令行工具和各种其他工具组合起来,用来完成各种复杂的操作。

正确的处理命令行参数对于写出高质量的命令行工具非常重要,那么如何正确的处理命令行参数呢?如果有 C 语言编程经验,或者用 C 语言写过命令行工具的人可能很熟悉 getopt(GNU getopt_long()),getopt 是 C Library 中一个专门用于解析命令行参数的工具,通常用 C 去写命令行工具的时候,getop 是一个很自然选择。

用 Ruby 写命令行工具

当使用 Ruby 写命令行工具的时候,我们在不借助任何内置/外置的命令行参数解析工具的情况下,可以直接从 ARGV 取到传入命令行的参数,然后手工判断,验证并执行后续操作。不过从遵循 Unix 的命令行工具的 Convertion 角度来讲,我不建议你直接从 ARGV 取数值,而是利用现有的库来作这件事情。Ruby 的标准库内置提供了一个 getopt 的 Ruby 实现 GetoptLongGetoptLong基本上模拟了 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 执行不同的操作,比如 gitpushpull 操作:

$ git push
$ git pull

就是两个完全不同的操作,但是他们的command部分都是git,只是action部分不同。我们也可以把这样的通过不同的action来实现不同的操作的命令行工具叫做 Command-Suit 工具,即从功能上看,它不是一个命令,而是一个命令的suit集合。Gli 就是帮助你快速实现这种Command-Suit的框架,如果你需要编写复杂的命令行工具,Gli 是一个不错的选择。

TrollopChoiceOptiflag都是命令行参数的 Ruby Parser,他们的目的一致,而且他们解析过程都遵循 Unix 的约定,只是实现各有不同,用法也不同,不过对我来说,他们都是一回事。就 Unix 命令行来说,参数只有 Options,Arguments,以及 Actions 而已,所以具体用哪个,看你的个人喜好,简单对比下来我认为Choice的 DSL 语法最易读,简洁,优雅,如果你需要这些第三方 Command Line parser 的时候,不妨考虑一下 Choice。不过我奉行另外一个原则,如果系统内置了的,我就不考虑第三方 gem,而且 Ruby 内置的 OptionParser 足够强大,能满足我对解析 Unix 的命令行参数的一切需求,所以我优选使用 OptionParser。这里我简单猜测一下为什么还有这么多第三方的轮子,第一是不知道 Ruby 已经内置了这个,第二个可能就是不爽 Ruby 内置的这个 parser 的文档或用法,虽然 OptionParser 足够强大灵活,但是不代表它好用,容易上手,相反,它的文档就相当坑爹!

用 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 命令行参数包含下面这些形式:

  • Option - Option 主要功能是用于调整命令行工具的行为,Option 的表现通常有两种形式,short option或者long option。Option 的类型有两种,switchflagswitch不带 argument,而flag带有 argument。
  • Argument - Argument 通常表示命令行工具要操作的对象,通常是路径,URL 或者名称等等。
  • Action - 表示命令行工具的行为,比如git命令的push或者pull等等。

举个例子git log --max-count=10git是 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 是怎么用的:

#!/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 点说明

  • 参数 opts.on('-s'和 opts.on('-s name'的区别是前者不用传参数
  • opts 只是用于解析参数的,而不是执行,执行代码应在 end.parse! 之后,把 options 作为参数传入

希望以上内容能够帮助你掌握写出符合 Unix 标准的命令行参数的工具,如果要写出易用,对用户友好,跟其他命令行工具互动良好,可测试,可维护,可格式化输出内容的真正awesome的命令行工具,您仍然需要继续努力,加油吧!