11 November 2014

前言

从Rails指南上了解到,Rails 3.0之后生成器,都是使用Thor开发的。于是,了解一下thor,翻译其官方介绍,原文链接: http://whatisthor.com/

Thor is a toolkit for building powerful command-line interfaces. It is used in Bundler, Vagrant, Rails and others.

Thor是构建强大命令行接口的工具箱。Bundler, Vagrant, Rails以及其他的一些项目使用了Thor构建其命令行工具。

bundler用来处理Ruby项目的版本依赖,Vagrant用来管理虚拟机运行环境,Rails是web开发框架。

Getting Started

Thor子类表述了带有一组子命令的可执行程序,比如 git 或 bundler。在Thor类中,public方法就是命令。

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  def hello(name)
    puts "Hello #{name}"
  end
end

通过调用MyCLI.start(ARGV)可以启动命令行CLI。通常而言,在gem包的bin目录下的可执行文件中,启动命令行。

如果出传递的参数为空,Thor默认打印出帮助文档。本文假设,读者在当前目录下存在一个包含如下的内容且名为cli的文件:

require "thor"
 
class MyCLI < Thor
  # contents of the Thor class
end
 
MyCLI.start(ARGV)

Thor在其子类的帮助提示中,自动使用执行文件名字。

$ ruby ./cli
 
Tasks:
  cli hello NAME   # say hello to NAME
  cli help [TASK]  # Describe available tasks or one specific task

执行hello任务可带有一个参数,Thor将自动调用对应的方法:

$ ruby ./cli hello Yehuda
Hello Yehuda

如果执行hello命令而不带有参数,Thor将自动打印一条有用的错误信息:

$ ruby ./cli hello
"hello" was called incorrectly. Call as "test.rb hello NAME".

You can also use Ruby’s optional arguments to make a CLI argument optional:

也可以在命令选项中使用Ruby的选项参数:

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  def hello(name, from=nil)
    puts "from: #{from}" if from
    puts "Hello #{name}"
  end
end

执行结果显示如下:

$ ruby ./cli hello "Yehuda Katz"
Hello Yehuda Katz
 
$ ruby ./cli hello "Yehuda Katz" "Carl Lerche"
from: Carl Lerche
Hello Yehuda Katz

这在某些情况下有用,但是,大多数情况下,只要使用Unix风格的选项即可。

长描述(Long Description)

默认情况下,Thor是desc提供简短的介绍:

$ ruby ./cli help hello
Usage:
  test.rb hello NAME
 
say hello to NAME

有些情况下,需要提供详尽的描述,提供如何使用详细指导。此时,需要使用long_desc来指定详细的使用指南。

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  long_desc <<-LONGDESC
    `cli hello` will print out a message to a person of your
    choosing.
 
    You can optionally specify a second parameter, which will print
    out a from message as well.
 
    > $ cli hello "Yehuda Katz" "Carl Lerche"
 
    > from: Carl Lerche
  LONGDESC
  def hello(name, from=nil)
    puts "from: #{from}" if from
    puts "Hello #{name}"
  end
end

默认情况下,长描述就是here文档,与markdown类似。 也可以在行的开头使用\x5转义序列,从而强制硬断行。

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  long_desc <<-LONGDESC
    `cli hello` will print out a message to a person of your
    choosing.
 
    You can optionally specify a second parameter, which will print
    out a from message as well.
 
    > $ cli hello "Yehuda Katz" "Carl Lerche"
    \x5> from: Carl Lerche
  LONGDESC
  def hello(name, from=nil)
    puts "from: #{from}" if from
    puts "Hello #{name}"
  end
end

有时,可能想要将长描述存储在单个文件中,从而保证CLI描述简短易读。 然后使用File.read去从文件中读取内容。

Options and Flags

Thor可以很容易的为命令指定选项和标志的元数据:

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  option :from
  def hello(name)
    puts "from: #{options[:from]}" if options[:from]
    puts "Hello #{name}"
  end
end

现在,用户可以指定from选项作为标志:

$ ruby ./cli hello --from "Carl Lerche" Yehuda
from: Carl Lerche
Hello Yehuda
 
$ ruby ./cli hello Yehuda --from "Carl Lerche"
from: Carl Lerche
Hello Yehuda
 
$ ruby ./cli hello Yehuda --from="Carl Lerche"
from: Carl Lerche
Hello Yehuda

默认情况下,选项是字符串类型,但也可以指定其他可用的类型:

class MyCLI < Thor
  option :from
  option :yell, :type => :boolean  # 布尔类型,出现即为ture,不出现为false
  desc "hello NAME", "say hello to NAME"
  def hello(name)
    output = []
    output << "from: #{options[:from]}" if options[:from]
    output << "Hello #{name}"
    output = output.join("\n")
    puts options[:yell] ? output.upcase : output
  end
end

现在,可以将任务的输出转换为大写:

$ ./cli hello --yell Yehuda --from "Carl Lerche"
FROM: CARL LERCHE
HELLO YEHUDA
 
$ ./cli hello Yehuda --from "Carl Lerche" --yell
FROM: CARL LERCHE
HELLO YEHUDA

也可以通过:required => true指定一个必须的选项。

class MyCLI < Thor
  option :from, :required => true
  option :yell, :type => :boolean
  desc "hello NAME", "say hello to NAME"
  def hello(name)
    output = []
    output << "from: #{options[:from]}" if options[:from]
    output << "Hello #{name}"
    output = output.join("\n")
    puts options[:yell] ? output.upcase : output
  end
end

此时,尝试不包含需要选项运行命令时,将会输出如下的报错信息:

$ ./cli hello Yehuda
No value provided for required options '--from'

选项可提供的全部的元数据的列表为:

  • :desc: 选项的描述。当使用cli help hello命令时,该描述将会在选项之后打印输出。
  • :banner: 选项的简短描述,以使用描述形式打印出来。默认情况下,是flag(from=FROM)的大写版本。
  • :required: 表明选项是必须的
  • :default: 选项的默认值。一般而言,:required 和 :default 成对出现
  • :type: 选项类型 :string, :hash, :array, :numeric, 或 :boolean
  • :aliases: 选项的别名,通常需要提供选项缩写版的别名

You can use a shorthand to specify a number of options at once if you just want to specify the type of the options. You could rewrite the previous example as:

使用简写形式,可以一次性为一组选项指定类型。 前面的例子,可以改写为:

class MyCLI < Thor
  desc "hello NAME", "say hello to NAME"
  options :from => :required, :yell => :boolean
  def hello(name)
    output = []
    output << "from: #{options[:from]}" if options[:from]
    output << "Hello #{name}"
    output = output.join("\n")
    puts options[:yell] ? output.upcase : output
  end
end

简写形式中,指定:required时,表明选项必须是:string类型。

Class Options

通过class_option可以为整个类指定选项。 类选项与单独的命令接受的参数相同,但其作用于类中的所有的命令。

给定任务中的选项hash将包含任何类类型。

class MyCLI < Thor
  class_option :verbose, :type => :boolean
 
  desc "hello NAME", "say hello to NAME"
  options :from => :required, :yell => :boolean
  def hello(name)
    puts "> saying hello" if options[:verbose]
    output = []
    output << "from: #{options[:from]}" if options[:from]
    output << "Hello #{name}"
    output = output.join("\n")
    puts options[:yell] ? output.upcase : output
    puts "> done saying hello" if options[:verbose]
  end
 
  desc "goodbye", "say goodbye to the world"
  def goodbye
    puts "> saying goodbye" if options[:verbose]
    puts "Goodbye World"
    puts "> done saying goodbye" if options[:verbose]
  end
end

Subcommands

随着CLI程序变得更加复杂时,某个特定的命令可能有其的一组子命令。 这里以git remote命令为例, 其包含add, rename, rm, prune, set-head等子命令。

在Thor中,可以通过创建一个新的Thor类来代替子命令, 并在其父类中指定一个关注点。以下,举例说明如何简化git remote的实现,并且例子是刻意简化的。

module GitCLI
  class Remote < Thor
    desc "add <name> <url>", "Adds a remote named <name> for the repository at <url>"
    long_desc <<-LONGDESC
      Adds a remote named <name> for the repository at <url>. The command git fetch <name> can then be used to create and update
      remote-tracking branches <name>/<branch>.
 
      With -f option, git fetch <name> is run immediately after the remote information is set up.
 
      With --tags option, git fetch <name> imports every tag from the remote repository.
 
      With --no-tags option, git fetch <name> does not import tags from the remote repository.
 
      With -t <branch> option, instead of the default glob refspec for the remote to track all branches under $GIT_DIR/remotes/<name>/, a
      refspec to track only <branch> is created. You can give more than one -t <branch> to track multiple branches without grabbing all
      branches.
 
      With -m <master> option, $GIT_DIR/remotes/<name>/HEAD is set up to point at remote's <master> branch. See also the set-head
      command.
 
      When a fetch mirror is created with --mirror=fetch, the refs will not be stored in the refs/remotes/ namespace, but rather
      everything in refs/ on the remote will be directly mirrored into refs/ in the local repository. This option only makes sense in
      bare repositories, because a fetch would overwrite any local commits.
 
      When a push mirror is created with --mirror=push, then git push will always behave as if --mirror was passed.
    LONGDESC
    option :t, :banner => "<branch>"
    option :m, :banner => "<master>"
    options :f => :boolean, :tags => :boolean, :mirror => :string
    def add(name, url)
      # implement git remote add
    end
 
    desc "rename <old> <new>", "Rename the remote named <old> to <new>"
    def rename(old, new)
    end
  end
 
  class Git < Thor
    desc "fetch <repository> [<refspec>...]", "Download objects and refs from another repository"
    options :all => :boolean, :multiple => :boolean
    option :append, :type => :boolean, :aliases => :a
    def fetch(respository, *refspec)
      # implement git fetch here
    end
 
    desc "remote SUBCOMMAND ...ARGS", "manage set of tracked repositories"
    subcommand "remote", Remote
  end
end

使用parent_options访问器,可以在子命令中访问父命令。

后记

去年留下的坑,今天(5月10号)填完。Thor在构建命令行上,还真是相当的简洁有效。使用类来对命令进行抽象。




傲娇的使用Disqus