2

如何使用 CLAide 开发命令行工具?

 2 years ago
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 开发命令行工具?

发表于 2021-11-21

|

更新于 2021-11-21

| 分类于 Ruby

CLAide 一文中,我们了解到 CocoaPods 是基于 CLAide 开发的一款依赖管理工具,也是一款命令行工具。本文,我们将基于 CLAide 开发一款简易的命令行工具——饮料制作器(BeverageMaker)。

在本项目中,我们将使用 bundler 作为项目管理工具和依赖管理工具,其实本质上就是使用 bundle 开发一个 gem 工具。

本文代码传送门 https://github.com/baochuquan/BeverageMaker

我们希望饮料制作器(BeverageMaker)能够制作两种类型的饮料:咖啡、茶。对此,我们需要分别实现两个子命令:coffeetea

对于咖啡,我们提供多种口味,如:BlackEye、Affogato、CaPheSuaDa、RedTux。对此,我们希望为 coffee 提供多个子命令,分别是:black-eyeaffogatoca-phe-sua-dared-tux

对于茶,我们提供多种口味,如:Black、Green、Oolong、White。这里,我们希望为 tea 提供多个参数,分别是:blackgreenoolongwhite

无论是咖啡还是茶,我们都希望两者支持选择是够添加牛奶、添加糖或蜂蜜作为甜味剂。对此,我们希望支持一个标志 --no-milk 表示是否添加牛奶,支持一个选项 --sweetener,其值为 superhoney

此外,对于茶,我们希望它支持一个额外的标志 --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 可以添加 buildinstallrelease 等任务。
    • 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::CommandCLAide::Command 的子类,作为根命令,它也是一个抽象类。由于,coffeetea 子类都支持 --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_commandtrue 将其置为抽象命令。

在构造方法 initialize 中,我们通过 argvARGV 类的实例) 读取对应的标志和选项进行实例化。

在校验方法 validate! 中,我们需要对 --sweetener 选项进行校验,因为它只支持 sugarhoney 两个值,当传入的值不符合预定义时,则抛出帮助提示。

在运行方法 run 中,我们定义饮料生产的通用逻辑,并根据 --no-milk 标志和 --sweetener 选项决定是否指定特殊逻辑。

实现子命令

接下来,我们需要实现子命令 coffeetea,为了能够对功能进行分类,我们新建一个 lib/BeverageMaker/command 目录,并新建 coffee.rbtea.rb 文件用于实现对应的命令类。

Coffee 子命令

Coffee 子命令类的定义如下所示,其继承自 BeverageMaker::Command 抽象类,内部通过设置 self.abstract_commandself.summaryself.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 类的子类,包括:BlackEyeAffogatoCoPheSuaDaRedTux。基于 Ruby 的语言特性,在运行时,这些子类会自动被标记为 Coffee 的子命令(内部由 self.inherited 命令支持)。这些子类各自定义了不同的命令摘要。

Tea 子命令

Tea 子命令类的定义如下所示,其继承自 BeverageMaker::Command 抽象类,内部通过设置 self.summaryself.description 等属性进行自定义配置。

Tea 子命令类覆写了 self.arguments 属性,这个属性定义了命令的参数,在打印帮助信息时,会使用这里所定义的内容。

Tea 子命令类覆写了 self.options 属性,这个属性定义了 tea 子命令所特有的选项 --iced

Tea 通过覆写构造方法 initializeargv 中按顺序读取参数和标志。

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 的整个项目结构、设计理念。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK