-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Thor
Thor 是什么?
用来构建命令行的工具包,是个 RubyGem。超过 200+ 以上的 Gems 皆选择采用 Thor 来打造命令行工具:如 Rails Generator、Vagrant、Bundler ..等。
gem install thor
一个 Thor 类别会成为可执行文件,类别内公有的实例方法便是子命令。
class MyCLI < Thor
desc "hello NAME", "say hello to NAME"
def hello(name)
puts "Hello #{name}"
end
end
用 MyCLI.start(ARGV)
来启动命令行工具,通常会将它放在 Gem 的 bin/
目录下。
若没传参数给 start
,默认会印出类别里的 help 信息。
举例,先创一个 cli
文件:
touch cli
,内容如下:
require "thor"
class MyCLI < Thor
desc "hello NAME", "say hello to NAME"
def hello(name)
puts "Hello #{name}"
end
end
MyCLI.start(ARGV)
执行这个文件:
$ ruby ./cli
Tasks:
cli hello NAME # say hello to NAME
cli help [TASK] # Describe available tasks or one specific task
传个参数看看:
$ ruby ./cli hello Juanito
Hello Juanito
参数数目不对怎么办?
$ ruby ./cli hello
"hello" was called incorrectly. Call as "test.rb hello NAME".
Thor 会帮你印出有用的错误信息。
也可让参数变成选择性传入。
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 "Juanito Fatas"
Hello Juanito Fatas
$ ruby ./cli hello "Juanito Fatas" "Ana Aguilar"
from: Ana Aguilar
Hello Juanito Fatas
Thor 默认使用命令上方的 desc
作为命令的简短说明。你也可以提供更完整的说明。使用 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 "Juanito Fatas" "Ana Aguilar"
> from: Ana Aguilar
LONGDESC
def hello(name, from=nil)
puts "from: #{from}" if from
puts "Hello #{name}"
end
end
默认 long_desc
会根据终端宽度断行,可以在行首加入 \x5
,如此便会在行与行之间加入 hard break。
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 "Juanito Fatas" "Ana Aguilar"
\x5> from: Ana Aguilar
LONGDESC
def hello(name, from=nil)
puts "from: #{from}" if from
puts "Hello #{name}"
end
end
多数情况下可将完整说明存至别的文件,并使用 File.read
读进来,这样可大幅提高整个 CLI 程式的可读性。
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 "Ana Aguilar" Juanito
from: Ana Aguilar
Hello Juanito
$ ruby ./cli hello Juanito --from "Ana Aguilar"
from: Ana Aguilar
Hello Juanito
Option 也可有类型。
class MyCLI < Thor
option :from
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
比如 --yell
是一个布尔选项。即使用者有给入 --yell
时,options[:yell]
为真、没给入时 options[:yell]
为假。
$ ./cli hello --yell juanito --from "Ana Aguilar"
FROM: ANA AGUILAR
HELLO JUANITO
$ ./cli hello juanito --from "Ana Aguilar" --yell
FROM: ANA AGUILAR
HELLO JUANTIO
位置可放前面或后面。
亦可指定某个参数是必须传入的。
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
如此例的 option :from, :required => true
,没给的话会提示如下错误:
$ ./cli hello Juanito
No value provided for required options '--from'
option
可传入的 metadata 清单:
-
:desc
option 的描述。使用 help 查看命令说明时,这里给入的文字,出现在 option 之后。 -
:banner
option 的短描述。没给的话,使用 help 查看命令说明时,会输出 flag 的大写,如from
就输出FROM
。 -
:required
表示这个选项是必要的。 -
:default
若 option 没给时的默认值。注意,:default
与required
互相冲突,不能一起用。 -
:type
有这五种::string
、:hash
、:array
、:numeric
、:boolean
。 -
:aliases:
此选项的别名。如--version
提供-v
。
上例若选项仅需指定类型时,可以写成一行:
option :from, :required => true
option :yell, :type => :boolean
等同于
option :from, :required, :yell => :boolean
:type
可用 :required
声明,会自动变成 :string
。
可以用 class_option
指定整个类共用的选项。跟一般选项接受的参数一样,但 class_option
对所有命令都生效。
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
命令日趋复杂时,会想拆成子命令,像 git remote
这样,git remote
是主命令、下面还有 add
、rename
、rm
、prune
、set-head
等子命令。
像 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
在 Git
类别中:
subcommand "remote", Remote
指定了 remote
为 Git
的子命令。
Remote
类别里的命令,可以透过 parent_options
选项来存取父命令的选项。
可以去研究 Bundler 的代码。