如何使用 CLAide 开发命令行工具?
在 CLAide 一文中,我们了解到 CocoaPods 是基于 CLAide 开发的一款依赖管理工具,也是一款命令行工具。本文,我们将基于 CLAide 开发一款简易的命令行工具——饮料制作器(BeverageMaker)。
在本项目中,我们将使用 bundler 作为项目管理工具和依赖管理工具,其实本质上就是使用 bundle 开发一个 gem 工具。
本文代码传送门 https://github.com/baochuquan/BeverageMaker。
目标
我们希望饮料制作器(BeverageMaker)能够制作两种类型的饮料:咖啡、茶。对此,我们需要分别实现两个子命令:coffee
、tea
。
对于咖啡,我们提供多种口味,如:BlackEye、Affogato、CaPheSuaDa、RedTux。对此,我们希望为
coffee
提供多个子命令,分别是:black-eye
、affogato
、ca-phe-sua-da
、red-tux
。
对于茶,我们提供多种口味,如:Black、Green、Oolong、White。这里,我们希望为
tea
提供多个参数,分别是:black
、green
、oolong
、white
。
无论是咖啡还是茶,我们都希望两者支持选择是够添加牛奶、添加糖或蜂蜜作为甜味剂。对此,我们希望支持一个标志
--no-milk
表示是否添加牛奶,支持一个选项
--sweetener
,其值为 super
或
honey
。
此外,对于茶,我们希望它支持一个额外的标志 --iced
表示是否加冰。
生成模版项目
首先,我们使用 bundle gem GEM_NAME
命令生成一个模版项目,项目名为:BeverageMaker
。
1 | bundle gem BeverageMaker |
上述命令会在当前目录下生成一个项目,项目的目录结构如下所示:
命令会自动生成一个脚手架模板项目,主要包含以下文件:
BeverageMaker.gemspec
:Gem Specification 文件,定义 Rubygem 的基本信息,如:名称、描述信息、gem 主页、 所需要的依赖等等。CODE_OF_CONDUCT.md
:关于代码贡献者需要遵循的行为准则。Gemfile
:用于管理项目依赖。该文件中有一行代码是gemspec
,其会调用BeverageMaker.gemspec
,从而导入项目的依赖项。因此,最佳实践是在gemspec
中指定项目所依赖的所有 gem。LICENSE.txt
:默认指定项目为 MIT 协议。Rakefile
:Ruby 中的构建脚本,类似于 C/C++ 中的 Makefile。通过Bundler::GemHelper.install_tasks
可以添加build
、install
、release
等任务。build
任务:构建当前版本的 gem,并将其存储在pkg
目录下。install
任务:构建 gem,并将其安装在我们的系统中。release
任务:将 gem 推送到 Rubygems,从而对外公开发布。
lib/BeverageMaker.rb
:定义 gem 代码的主文件。当 gem 被加载时,Bundler 会请求这个文件。该文件定义了一个module
,其可以作为 gem 代码的命名空间。因此,最佳实践是把代码定义在module
中。lib/BeverageMaker
目录:该目录下包含了 gem 的所有代码。lib/BeverageMaker.rb
文件用于设置 gem 的环境,而它的所有代码都在lib/BeverageMaker
目录下。如果 gem 有多种功能,我们可以进一步对其拆分子目录。lib/BeverageMaker/version.rb
:内部通过一个VERSION
常量定义 gem 的版本。文件由BeverageMaker.gemspec
进行加载,从而为 gem 指定版本。当发布新版本时,可以修改VERSION
的值来指定新的版本号。spec
目录:用于存放测试文件。
修改 gemspec 配置
初始化模板项目后,我们需要对 BeverageMaker.gemspec
文件所包含的 TODO
字段进行替换。此外,我们还需要添加项目依赖:'claide', '>= 1.0.2', '< 2.0'
和 'colored2', '~> 3.1'
。
colored2
用于 banner 信息的 ANSI 转义,从而能够支持以富文本格式在终端输出。
修改后的 BeverageMaker.gemspec
配置如下所示:
1 | require_relative 'lib/BeverageMaker/version' |
定义命令行入口
我们通过修改 BeverageMaker.gemspec
来定义命令行入口的位置和名称,如下所示:
1 | spec.bindir = "exe" |
模板自动生成的 BeverageMaker.gemspec
配置中,bindir
默认为
exe
,表示二进制(binary)文件的存储目录为项目根目录下的
exe
目录;executables
默认为
spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
,表示
exe
目录下的所有文件。我们可以将其改写成指定名称
beverage-maker
,该文件也是命令行的入口文件。
定义好命令行的入口与名称后,我们需要在 exe
目录下创建一个同名的文件
beverage-maker
,将其作为命令行入口,其定义如下:
1
2
3
4
5
require 'BeverageMaker'
BeverageMaker::Command.run(ARGV)
注意,由于 beverage-maker
是可执行文件,需要确保其具有可执行权限,我们通过执行
chmod +x beverage-maker
命令为其添加可执行权限。
实现根命令
上述命令入口中,BeverageMaker::Command
是
CLAide::Command
的子类,作为根命令,它也是一个抽象类。由于,coffee
和
tea
子类都支持 --no-milk
和
--sweetener
选项,我们可以将这些选项定义在根命令的类中。
对此,我们新建一个文件 command.rb
,并定义
Beverage::Command
根命令,如下所示: 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51# command.rb
module BeverageMaker
require 'claide'
class Command < CLAide::Command
require 'beveragemaker/command/coffee'
require 'beveragemaker/command/tea'
self.abstract_command = true
self.description = 'Make delicious beverages from the comfort of your' \
'terminal.'
# This would normally default to `beverage-maker`, based on the class’ name.
self.command = 'beverage-maker'
def self.options
[
['--no-milk', 'Don’t add milk to the beverage'],
['--sweetener=[sugar|honey]', 'Use one of the available sweeteners'],
].concat(super)
end
def initialize(argv)
@add_milk = argv.flag?('milk', true)
@sweetener = argv.option('sweetener')
super
end
def validate!
super
if @sweetener && !%w(sugar honey).include?(@sweetener)
help! "`#{@sweetener}' is not a valid sweetener."
end
end
def run
puts '* Boiling water…'
sleep 1
if @add_milk
puts '* Adding milk…'
sleep 1
end
if @sweetener
puts "* Adding #{@sweetener}…"
sleep 1
end
end
end
endself.options
自定义支持的选项:--no-milk
和
--sweetener
。通过设置 self.description
指定命令的描述信息。通过设置 self.abstract_command
为
true
将其置为抽象命令。
在构造方法 initialize
中,我们通过
argv
(ARGV
类的实例)
读取对应的标志和选项进行实例化。
在校验方法 validate!
中,我们需要对
--sweetener
选项进行校验,因为它只支持 sugar
和 honey
两个值,当传入的值不符合预定义时,则抛出帮助提示。
在运行方法 run
中,我们定义饮料生产的通用逻辑,并根据
--no-milk
标志和 --sweetener
选项决定是否指定特殊逻辑。
实现子命令
接下来,我们需要实现子命令 coffee
和
tea
,为了能够对功能进行分类,我们新建一个
lib/BeverageMaker/command
目录,并新建
coffee.rb
和 tea.rb
文件用于实现对应的命令类。
Coffee 子命令
Coffee
子命令类的定义如下所示,其继承自
BeverageMaker::Command
抽象类,内部通过设置
self.abstract_command
、self.summary
、self.description
等属性进行自定义配置。通过覆写 run
方法,定义制作咖啡时的特定逻辑,注意内部会调用 super
以执行饮料制作的通用逻辑。
1 | # coffee.rb |
此外,我们还定义了 Coffee
类的子类,包括:BlackEye
、Affogato
、CoPheSuaDa
、RedTux
。基于
Ruby 的语言特性,在运行时,这些子类会自动被标记为 Coffee
的子命令(内部由 self.inherited
命令支持)。这些子类各自定义了不同的命令摘要。
Tea 子命令
Tea
子命令类的定义如下所示,其继承自
BeverageMaker::Command
抽象类,内部通过设置
self.summary
、self.description
等属性进行自定义配置。
Tea
子命令类覆写了 self.arguments
属性,这个属性定义了命令的参数,在打印帮助信息时,会使用这里所定义的内容。
Tea
子命令类覆写了 self.options
属性,这个属性定义了 tea
子命令所特有的选项
--iced
。
Tea
通过覆写构造方法 initialize
从
argv
中按顺序读取参数和标志。
在 validate!
方法中,它对口味参数进行校验,判断其是否属于预定义的几种类型之一,如果不符合,则打印帮助提示。
在 run
方法,定义制作茶饮的特定逻辑,注意内部会调用
super
以执行饮料制作的通用逻辑。 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50# tea.rb
module BeverageMaker
# This command uses an argument for the extra parameter, instead of
# subcommands for each of the flavor.
class Tea < Command
self.summary = 'Drink based on cured leaves'
self.description = <<-DESC
An aromatic beverage commonly prepared by pouring boiling hot
water over cured leaves of the Camellia sinensis plant.
The following flavors are available: black, green, oolong, and white.
DESC
self.arguments = [
CLAide::Argument.new('FLAVOR', true),
]
def self.options
[['--iced', 'the ice-tea version']].concat(super)
end
def initialize(argv)
@flavor = argv.shift_argument
@iced = argv.flag?('iced')
super
end
def validate!
super
if @flavor.nil?
help! 'A flavor argument is required.'
end
unless %w(black green oolong white).include?(@flavor)
help! "`#{@flavor}' is not a valid flavor."
end
end
def run
super
puts "* Infuse #{@flavor} tea…"
sleep 1
if @iced
puts '* Cool off…'
sleep 1
end
puts '* Enjoy!'
end
end
end
加载根命令
在 exe/beverage-maker
中,我们通过
require 'BeverageMaker'
导入 gem,此时 Bundler 会请求同名的
BeverageMaker.rb
文件,而 BeverageMaker.rb
中并没有 exe/beverage-maker
所需的
BeverageMaker::Command
类。因此我们需要加载
BeverageMaker::Command
类,我们可以通过
autoload
方法加载,如下所示。
1 | # BeverageMaker.rb |
调试
本文使用 RubyMine 进行开发,RubyMine
提供了一系列调试功能,我们可以选中 exe/beverage-maker
文件,右击选择【Debug 'beverage-maker'】,RubyMine 将自动以调试模式运行
beverage-maker
。
运行 beverage-maker
后,控制台将输出运行结果,如下所示:
由于 beverage-maker
是入口程序,我们可以修改其代码,传入不同的参数进行调试,如下所示:
1 |
|
其调试运行结果如下所示:
构建
在调试通过后,我们可以对 gem 进行构建,构建命令如下:
1
rake build
构建命令会根据 BeverageMaker.gemspec
生成一个对应版本的
gem,这里生成的是 BeverageMaker-0.1.0.gem
,并存放在
pkg
目录下。
注:也可以使用
gem build BeverageMaker.gemspec
进行构建。
安装
在构建生成 gem 后,我们可以对它进行安装,安装命令如下:
1
rake install
安装命令默认将 gem 安装在当前使用的 ruby 版本的目录下,我当前的 ruby
版本是 2.6.5
。对此,安装结果如下。
- 可执行文件
beverage-maker
安装在~/.rvm/gems/ruby-2.6.5/bin/
目录下 - BeverageMaker gem 包安装在
~/.rvm/gems/ruby-2.6.5/gems/
目录下 - BeverageMaker 的 gemspec 安装在
~/.rvm/gems/ruby-2.6.5/specifications/
目录下。
注:也可以使用
gem install pkg/BeverageMaker-0.1.0.gem
命令进行安装。
安装完毕,我们就可以在控制台中使用 beverage-maker
命令行工具了。
发布
一切就绪后,我们可以对 gem 进行发布,发布命令如下:
1
rake release
rake release
发布命令包含几个步骤:
- 构建 gem,并存放至
pkg
目录下,准备推送至 Rubygems.org。 - 在当前的 commit 上打上 tag,指定为当前版本号。
- 将代码推送至远程的 git 仓库。
执行结果如下所示:
注:也可以使用
gem push pkg/BeverageMaker-0.1.0.gem
命令进行发布。
在发布前,我们需要注册一个 rubygems.org 的账户,否则发布命令会报错。
总结
本文通过一个具体的需求,基于 CLAide 开发了一个 gem 命令行工具。在这个过程中,我们介绍了模板项目中各个文件的作用,了解了 gem 开发的一般步骤:开发、调试、构建、安装、发布。
另一方面,通过这样一个项目,我们大致也能够理解 cocoapods 的整个项目结构、设计理念。