![](/style/images/good.png)
![](/style/images/bad.png)
如何使用 CLAide 开发命令行工具?
source link: http://chuquan.me/2021/11/21/how-to-develop-command-line-tool-with-claide/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
如何使用 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
。
$ 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
配置如下所示:
require_relative 'lib/BeverageMaker/version'
Gem::Specification.new do |spec|
spec.name = "BeverageMaker"
spec.version = BeverageMaker::VERSION
spec.authors = ["baochuquan"]
spec.email = ["[email protected]"]
spec.summary = "Beverage Maker"
spec.description = "A Command Line Tool Example"
spec.homepage = "https://github.com/baochuquan/BeverageMaker"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/baochuquan/BeverageMaker"
spec.metadata["changelog_uri"] = "https://github.com/baochuquan/BeverageMaker"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = "beverage-maker"
spec.require_paths = ["lib"]
spec.add_dependency 'claide', '~> 1.0.3'
end
定义命令行入口
我们通过修改 BeverageMaker.gemspec
来定义命令行入口的位置和名称,如下所示:
spec.bindir = "exe"
spec.executables = "beverage-maker"
模板自动生成的 BeverageMaker.gemspec
配置中,bindir
默认为 exe
,表示二进制(binary)文件的存储目录为项目根目录下的 exe
目录;executables
默认为 spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
,表示 exe
目录下的所有文件。我们可以将其改写成指定名称 beverage-maker
,该文件也是命令行的入口文件。
定义好命令行的入口与名称后,我们需要在 exe
目录下创建一个同名的文件 beverage-maker
,将其作为命令行入口,其定义如下:
#!/usr/bin/env ruby
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
根命令,如下所示:
# 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
end
我们通过覆写 self.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
以执行饮料制作的通用逻辑。
# coffee.rb
module BeverageMaker
# Unlike the Tea command, this command uses subcommands to specify the
# flavor.
#
# Which one makes more sense is up to you.
class Coffee < Command
self.abstract_command = true
self.summary = 'Drink brewed from roasted coffee beans'
self.description = <<-DESC
Coffee is a brewed beverage with a distinct aroma and flavor
prepared from the roasted seeds of the Coffea plant.
DESC
def run
super
puts "* Grinding #{self.class.command} beans…"
sleep 1
puts '* Brewing coffee…'
sleep 1
puts '* Enjoy!'
end
class BlackEye < Coffee
self.summary = 'A Black Eye is dripped coffee with a double shot of ' \
'espresso'
end
class Affogato < Coffee
self.summary = 'A coffee-based beverage (Italian for "drowned")'
end
class CaPheSuaDa < Coffee
self.summary = 'A unique Vietnamese coffee recipe'
end
class RedTux < Coffee
self.summary = 'A Zebra Mocha combined with raspberry flavoring'
end
end
end
此外,我们还定义了 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
以执行饮料制作的通用逻辑。
# 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
方法加载,如下所示。
# BeverageMaker.rb
require "BeverageMaker/version"
module BeverageMaker
class Error < StandardError; end
# Your code goes here...
autoload :Command, 'BeverageMaker/command'
end
本文使用 RubyMine 进行开发,RubyMine 提供了一系列调试功能,我们可以选中 exe/beverage-maker
文件,右击选择【Debug ‘beverage-maker’】,RubyMine 将自动以调试模式运行 beverage-maker
。
运行 beverage-maker
后,控制台将输出运行结果,如下所示:
由于 beverage-maker
是入口程序,我们可以修改其代码,传入不同的参数进行调试,如下所示:
#!/usr/bin/env ruby
require 'BeverageMaker'
# BeverageMaker::Command.run(ARGV)
BeverageMaker::Command.run(["tea", "oolong", "--iced", "--no-milk"])
print"\n"
BeverageMaker::Command.run(["coffee", "ca-phe-sua-da", "--sweetener=sugar"])
其调试运行结果如下所示:
在调试通过后,我们可以对 gem 进行构建,构建命令如下:
$ rake build
构建命令会根据 BeverageMaker.gemspec
生成一个对应版本的 gem,这里生成的是 BeverageMaker-0.1.0.gem
,并存放在 pkg
目录下。
注:也可以使用
gem build BeverageMaker.gemspec
进行构建。
在构建生成 gem 后,我们可以对它进行安装,安装命令如下:
$ 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 进行发布,发布命令如下:
$ 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 的整个项目结构、设计理念。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK