把 Ruby 小工具打包成带测试和命令行入口的 gem

把 Ruby 小工具打包成带测试和命令行入口的 gem

原文:RubyGems documentation contributors,Make your own gem。中文翻译与技术整理:未完纪。本文按 2026 年 10 月 9 日核对的官方指南翻译整理;下列终端输出是原文示例,不代表所有环境都会得到相同输出。

一小段 Ruby 代码可以直接放进项目,但要在另一个项目使用或交给别人时,反复复制就开始变得麻烦。gem 把库代码、版本、依赖和必要元数据放在一个可分发包中。使用者只需 gem install,或在 Gemfile 中增加声明,就能复用它。本文从一个 Hola 类开始,走到多文件结构、测试和命令行入口,最后解释官方指南中的发布流程。

Ruby gem从lib代码和gemspec经测试与构建形成gem包,再进行本地安装及命令行调用的流程
从代码到本地可安装产物;本文绘制的流程示意,不是终端截图。

第一个 gem:代码与规格文件

最小项目只需要 lib/hola.rb 和 hola.gemspec。按照约定,lib 下放一个与 gem 同名的 Ruby 文件;require “hola” 加载它,由它负责建立库的入口和公开 API。hola 是原文已有的示例包名,若日后公开发布,必须选择可用且属于自己的名字,例如带有自己标识的名称。命名建议见 Name your gem。

$ tree
.
├── hola.gemspec
└── lib
    └── hola.rb
$ cat lib/hola.rb
class Hola
  def self.hi
    puts "Hello world!"
  end
end

这段最初的 hi 方法用 puts 输出问候,因此返回 nil。后面加入 Translator 时会改为返回字符串,这个区别会影响测试与命令行包装。gemspec 描述包中有哪些文件、作者是谁以及当前版本。RubyGems.org 包页面上显示的大部分元数据也来自它。

$ cat hola.gemspec
Gem::Specification.new do |s|
  s.name        = "hola"
  s.version     = "0.0.0"
  s.summary     = "Hola!"
  s.description = "A simple hello world gem"
  s.authors     = ["Nick Quaranto"]
  s.email       = "nick@quaran.to"
  s.files       = ["lib/hola.rb"]
  s.homepage    =
    "https://rubygems.org/gems/hola"
  s.license       = "MIT"
end

这里的 Nick Quaranto、邮箱、主页和 MIT 是原文 hola 项目的示例信息,不能直接用于自己的项目。自己的包应填写实际作者、联系地址、代码主页和真实许可。尤其 s.license = “MIT” 只描述这个示例 gem,不代表整篇指南的文档许可证。

gemspec 本身就是 Ruby,可以用 Ruby 代码生成文件列表或读取版本常量。description 可比示例更长;原文说明,匹配 /^== [A-Z]/ 的描述会在 RubyGems 网站上经 RDoc 标记格式化,但其他使用该数据的工具未必理解此格式。所有字段可查 Specification reference。

有了 gemspec,就能构建 .gem 文件并安装到本机。下面是原文的构建、安装和 IRB 示例记录,实际输出会随环境而不同。

$ gem build hola.gemspec
  Successfully built RubyGem
  Name: hola
  Version: 0.0.0
  File: hola-0.0.0.gem

$ gem install ./hola-0.0.0.gem
Successfully installed hola-0.0.0
Parsing documentation for hola-0.0.0
Installing ri documentation for hola-0.0.0
Done installing documentation for hola after 0 seconds
1 gem installed
$ irb
3.1.2 :001 > require "hola"
=> true
3.1.2 :002 > Hola.hi
Hello world!
=> nil

require 成功说明入口能加载,Hola.hi 的输出说明代码可调用。首次例子返回 nil 是 puts 的行为,不能把它当作后续返回字符串实现的预期值。示例 IRB 提示符显示 Ruby 3.1.2,仅是原文记录的环境,不是本文声明的统一最低版本。

用 Bundler 生成新项目

手工搭建有助于理解结构;新项目也可以使用官方指南推荐的 bundle gem 脚手架。foodie 是这一节另一个独立示例名,不需要把前面的 hola 项目强行改名。

$ bundle gem foodie

生成的 Gemfile 管理开发依赖,其中的 gemspec 行还会让 Bundler 读取规格文件中的依赖。运行时依赖应写在 gemspec;与测试和开发相关的依赖,项目已有 Gemfile 时优先写在 Gemfile。Rakefile 通过 Bundler::GemHelper.install_tasks 加入 build、install 和 release 任务;foodie.gemspec 等待补齐 description、homepage、metadata[“source_code_uri”] 与 metadata[“changelog_uri”];lib/foodie.rb 是入口,lib/foodie/version.rb 定义版本常量。生成的 .gitignore 忽略 pkg、.gem 和 .bundle 等产物。

命令还会询问是否加入 CODE_OF_CONDUCT.md 和 LICENSE.txt。确定自己的许可后,再完成这些文件。生成项目可用下面的 Rake 任务构建和本地安装。

$ rake build    # Build the gem into the pkg directory
$ rake install  # Build and install the gem to your system

拆出 Translator,保持入口清楚

把所有功能堆在一个文件里不利于维护。先让 Hola.hi 接受语言参数,并把选择问候语的逻辑放入 Hola::Translator。此时 hi 返回字符串,而不是直接输出。

$ cat lib/hola.rb
class Hola
  def self.hi(language = "english")
    translator = Translator.new(language)
    translator.hi
  end
end

class Hola::Translator
  def initialize(language)
    @language = language
  end

  def hi
    case @language
    when "spanish"
      "hola mundo"
    else
      "hello world"
    end
  end
end

随着代码增多,把 Translator 放入 lib/hola/translator.rb;其余库文件一般放在 lib 下与 gem 同名的目录中。入口文件负责加载它们。

$ tree
.
├── hola.gemspec
└── lib
    ├── hola
    │   └── translator.rb
    └── hola.rb
$ cat lib/hola/translator.rb
class Hola::Translator
  def initialize(language)
    @language = language
  end

  def hi
    case @language
    when "spanish"
      "hola mundo"
    else
      "hello world"
    end
  end
end
$ cat lib/hola.rb
class Hola
  def self.hi(language = "english")
    translator = Translator.new(language)
    translator.hi
  end
end

require 'hola/translator'

注意先定义 Hola,再 require 子文件,才能解析 class Hola::Translator。增加文件后必须同步更新 gemspec 的 files;漏掉的新文件不会进入安装包,源目录能运行并不代表分发包完整。

$ cat hola.gemspec
Gem::Specification.new do |s|
...
  s.files       = ["lib/hola.rb", "lib/hola/translator.rb"]
...
end

开发目录可以用 irb -Ilib -rhola 直接试用。-Ilib 把 lib 加入加载路径,-rhola 预加载入口;安装后的使用者通常不必自己设置这些路径。一般不要在库内部随意修改 $LOAD_PATH。

$ irb -Ilib -rhola
3.1.2 :001 >  Hola.hi("english")
=> "hello world"
3.1.2 :002 > Hola.hi("spanish")
=> "hola mundo"

以后增加目录和代码仍按同样方式拆分。文件清单可借助 Hoe、Jeweler、Rake、lorem 或动态 gemspec 自动维护,但自动化也要确保只包含应该发布的文件,不能把本机凭据或其他开发私有文件打入包中。更多组织方式见 Patterns。

声明运行时与开发依赖

库在运行时需要另一个 gem,就在 gemspec 中用 add_dependency 声明。例如 activesupport 的版本约束可以写成:

Gem::Specification.new do |s|
  ...
  s.add_dependency "activesupport", ">= 7.0"
end

原文建议多数情况下使用 >= 这样的开放下界,避免无端阻止使用者升级;存在真实兼容性边界时,再选择 ~> 等约束。不要仅凭示例假定未来所有版本都兼容,应根据项目支持范围制定约束。

仅测试时需要的包可以声明为开发依赖。原文给出的独立 gemspec 写法如下;如果项目同时有 Gemfile,指南建议把开发依赖放在 Gemfile。bundle install 解析并安装项目声明的依赖,gem install 包名 –dev 会额外安装开发依赖。

s.add_development_dependency "minitest", ">= 5.0"

为库行为写测试

测试不仅检查自己的实现,也帮助使用者判断库是否可靠。gem 允许把测试文件一并打包。官方指南分别给出 Minitest 和 RSpec 两条路线,可选择其中一种,不要求在小项目里同时维护两套重复测试。原文把 Minitest 描述为 Ruby 内置框架;实际可用性依 Ruby 发行环境而定,因此这里保留显式声明开发依赖的步骤。

Minitest 路线增加 Rakefile 和 test/test_hola.rb。目录示例中的 bin/hola 将在下一节建立。

$ tree
.
├── Rakefile
├── bin
│   └── hola
├── hola.gemspec
├── lib
│   ├── hola
│   │   └── translator.rb
│   └── hola.rb
└── test
    └── test_hola.rb
$ cat Rakefile
require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "test"
end

desc "Run tests"
task default: :test

Rake::TestTask 提供 test 任务,默认任务也指向它。测试分别验证 english、未识别的 ruby 参数,以及 spanish。未识别语言回退到英语,正对应 Translator 中的 else 分支。

$ cat test/test_hola.rb
require "minitest/autorun"
require "hola"

class HolaTest < Minitest::Test
  def test_english_hello
    assert_equal "hello world",
      Hola.hi("english")
  end

  def test_any_hello
    assert_equal "hello world",
      Hola.hi("ruby")
  end

  def test_spanish_hello
    assert_equal "hola mundo",
      Hola.hi("spanish")
  end
end

执行 rake test 或 rake 可运行这套测试。原文的演示报告如下;时间、随机种子和速度属于那次示例输出,不能作为当前机器的性能或已通过证据。

$ rake test
Run options: --seed 9351

# Running:

...

Finished in 0.005645s, 531.4108 runs/s, 531.4108 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

RSpec 路线先增加开发依赖,再创建 spec/hola_spec.rb。使用 Bundler 的项目可以将依赖写入 Gemfile 并安装后,用 bundle exec 保持执行环境与项目依赖一致。

s.add_development_dependency "rspec", "~> 3.0"
$ tree
.
├── hola.gemspec
├── lib
│   ├── hola
│   │   └── translator.rb
│   └── hola.rb
└── spec
    └── hola_spec.rb
$ cat spec/hola_spec.rb
require "hola"

describe Hola do
  it "says hello world in english" do
    expect(Hola.hi("english")).to eql("hello world")
  end

  it "says hello world by default" do
    expect(Hola.hi("ruby")).to eql("hello world")
  end

  it "says hola mundo in spanish" do
    expect(Hola.hi("spanish")).to eql("hola mundo")
  end
end
$ bundle exec rspec spec

3 examples, 0 failures

三个 RSpec 例子与 Minitest 检查同样的行为。原文中的 “by default” 用例实际传入了 “ruby”,验证的是未知语言回退,不是无参数调用;如果要单独证明默认参数,应另写 Hola.hi 的断言。

给 gem 加一个命令行入口

gem 除了提供 Ruby 库,还能把一个或多个可执行入口安装到 shell 可找到的位置。rake 是典型例子,Nokogiri 也带有用于交互解析 HTML/XML 的命令:

$ gem install -N nokogiri
[...]
$ nokogiri https://www.ruby-lang.org/
Your document is stored in @doc...
3.1.2 :001 > @doc.title
=> "Ruby Programming Language"

为 Hola 添加入口,在 bin 目录中新建 hola 文件。在类 Unix 系统上,下列命令创建文件并赋予执行权限;其他平台按自身文件权限机制处理,Ruby 直接调用脚本的方式仍可用。

$ mkdir bin
$ touch bin/hola
$ chmod a+x bin/hola

shebang 告诉系统使用 Ruby 执行文件。脚本 require 库,再把第一个命令行参数作为语言传给 Hola.hi,由入口统一 puts 输出。这样库方法继续返回字符串,测试也不必捕获标准输出。没有参数时 ARGV[0] 为 nil,Translator 的 else 分支仍返回英语。

$ cat bin/hola
#!/usr/bin/env ruby

require 'hola'
puts Hola.hi(ARGV[0])
$ ruby -Ilib ./bin/hola
hello world

$ ruby -Ilib ./bin/hola spanish
hola mundo

还要在 gemspec 的 executables 中声明入口名,名字不包含 bin/ 前缀。原文用下面的开头展示新增字段和版本变化,省略部分仍应保留完整 gemspec。

$ head -4 hola.gemspec
Gem::Specification.new do |s|
  s.name        = "hola"
  s.version     = "0.0.1"
  s.executables << "hola"

编辑整理:构建最终产物时,应把 files 清单和入口文件一起核查,例如包含 lib/hola.rb、lib/hola/translator.rb、bin/hola 及实际存在的许可文件。gemspec 示例使用明确清单时,新增源文件必须同步维护。每次发布新版本都要改变版本号,不能拿修改后的内容重复发布同一个版本。

写文档时不要意外改变返回值

多数 gem 使用 RDoc 生成 API 文档。给类和方法写出用途、参数及调用结果,就能得到可读的参考内容。官方页面的 RDoc 示范片段如下:

# The main Hola driver
class Hola
  # Say hi to the world!
  #
  # Example:
  #   >> Hola.hi("spanish")
  #   => hola mundo
  #
  # Arguments:
  #   language: (String)

  def self.hi(language = "english")
    translator = Translator.new(language)
    puts translator.hi
  end
end

返回值差异:这段原文在最后调用 puts translator.hi,使 Hola.hi 返回 nil,与前面测试示例期待的字符串不同。主线实现继续使用前面的 translator.hi。下面的说明性版本只移除 puts,使方法返回字符串。

# 问候语库入口。
class Hola
  # 根据语言返回问候语。
  # 例:Hola.hi("spanish") # => "hola mundo"
  # language:语言名称字符串
  def self.hi(language = "english")
    translator = Translator.new(language)
    translator.hi
  end
end
require "hola/translator"

另一种选择是 YARD。原文说明 RubyDoc.info 可为发布的 gem 生成 YARD 文档,YARD 也兼容 RDoc 的常见标记。选择工具时保持代码示例与真实 API 行为一致,比更换文档生成器更重要。

理解发布流程,区分本地操作与公开发布

上面的示例覆盖本地构建、测试和安装。以下说明如何继续发布到 RubyGems.org;发布前需要账户和可用且由自己控制的 gem 名称。

gem signin 会交互式要求邮箱、密码和启用 MFA 时的 OTP,并让你选择 API 密钥权限。下面是原文示范提示;邮箱、时间串和 123456 都是演示内容,不是可用凭据。权限示例只选择推送所需的 push_rubygem。

$ gem signin
Enter your RubyGems.org credentials.
Don't have an account yet? Create one at https://rubygems.org/sign_up
  Email:   (your-email-address@example.com)
Password:   (your password for RubyGems.org)

API Key name [host-user-20220102030405]:
Please select scopes you want to enable for the API key (y/n)
index_rubygems [y/N]:   n
push_rubygem [y/N]:   y
yank_rubygem [y/N]:   n
add_owner [y/N]:   n
remove_owner [y/N]:   n
access_webhooks [y/N]:   n
show_dashboard [y/N]:   n

You have enabled multi-factor authentication. Please enter OTP code.
Code:   123456
Signed in with API key: host-user-20220102030405.

原文还提到浏览器下载 api_key.yaml 并保存为 ~/.gem/credentials 的旧式排障办法,但未在该段完整说明具体下载入口与文件权限。本文不把这段不完整线索改写成可直接执行的凭据获取命令;遇到 TLS 或证书问题,应按 官方 TLS 排障指南处理,不应通过关闭证书验证绕过问题。凭据文件不得加入仓库或 gem 包。

登录完成后,gem push 会把指定 .gem 文件上传到公开服务。原文展示成功注册与后续远程安装,保留如下作为阅读材料;发布当前最终示例时必须用自己的包名和实际构建出的版本文件,不能原样发布 hola。

$ gem push hola-0.0.0.gem
Pushing gem to RubyGems.org...
Successfully registered gem: hola (0.0.0)
$ gem list -r hola

*** REMOTE GEMS ***

hola (0.1.3)

$ gem install hola
Fetching hola-0.1.3.gem
Successfully installed hola-0.1.3
Parsing documentation for hola-0.1.3
Installing ri documentation for hola-0.1.3
Done installing documentation for hola after 0 seconds
1 gem installed

如果项目由 bundle gem 生成,rake release 会构建到 pkg、创建当前版本的 Git 标签、推送标签到远程,再将包推送到 RubyGems.org。这包含两个外部写入动作,不是普通本地测试命令。发布前应修改版本文件,例如 lib/hola/version.rb,并提交全部预期变更。

官方指南还列出 gem-release 工具帮助提升版本号的命令。下面是官方示例语法;使用这类工具前,应先核对其来源及对本地版本文件的修改行为:

$ gem install gem-release
$ gem bump --version minor  # bumps to the next minor version
$ gem bump --version major  # bumps to the next major version
$ gem bump --version 1.1.1  # bumps to the specified version

署名、来源与适用范围

本文覆盖官方指南的引言、首次构建、Bundler 脚手架、多文件加载、依赖、Minitest/RSpec、命令行入口、文档及两种发布方式。返回值修订和环境注意事项在相应位置标出。示例输出来自原文,不能用作当前环境的验证结果。

原文教程题为 Make your own gem,由 RubyGems Guides 贡献者编写,并改编自 Gem Sawyer, Modern Day Ruby Warrior;Hola 示例代码作者为 Nick Quaranto。本文是中文翻译与整理,包含结构调整、返回值说明及发布安全边界。RubyGems Guides 将文章内容按 Creative Commons 提供,其 CC-LICENSE列出署名与相同方式共享条款;本文译文按该上游许可发布。仓库实现代码另有 MIT-LICENSE,与文章内容许可不同;示例 gemspec 的 MIT 字段只描述示例 gem。原创流程图单独取得发布授权,不随文章 CC 条款许可。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容