CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Ruby 语言入门/课时/综合练习
RawBlame
课时目录12 节

Ruby 语言入门

  1. 01Ruby 环境
  2. 02数据类型
  3. 03语法基础
  4. 04控制流
  5. 05方法与 Blocks
  6. 06OOP 与 Mixin
  7. 07元编程
  8. 08IO 与 Enumerable
  9. 09并发
  10. 10Gems 与框架
  11. 11测试与质量
  12. 12综合练习

综合练习

CLI + Gem 小项目。

30 分钟

综合练习

这一课你将能做到

  1. 用 Bundler 创建一个标准 Gem 项目结构,理解 gemspec 和 lib/exe 目录分工
  2. 写一个完整的 CLI 工具,包含参数解析、文件处理和错误处理
  3. 为项目配置 RSpec 测试和 RuboCop 检查,达到可发布的质量标准

为什么要学

前面 11 课学了 Ruby 的语法、OOP、元编程、并发、生态——但「知道」和「做到」之间隔着一条鸿沟。这一课把全部知识串起来,写一个真实可用的 CLI 工具,体验从「零」到「可发布 Gem」的完整流程。

人话:前面学的是「单词和语法」,这一课是「写作文」。Bundler 是稿纸格式,CLI 是文章结构,测试是校对,RuboCop 是排版——一篇能发表的文章,每个环节都不能少。

先建立图像

标准 Gem 项目结构
my_tool/
├── Gemfile              # Bundler 依赖声明
├── Gemfile.lock         # 锁定版本
├── Rakefile             # 任务定义(测试、发布)
├── my_tool.gemspec      # Gem 元数据(名称、版本、依赖)
├── lib/
│   ├── my_tool.rb       # 主入口
│   └── my_tool/
│       ├── version.rb   # 版本常量
│       └── cli.rb       # CLI 逻辑
├── exe/
│   └── my_tool          # 可执行文件(binstub)
├── spec/
│   ├── spec_helper.rb   # RSpec 配置
│   └── my_tool_spec.rb  # 测试
└── .rubocop.yml         # 代码风格配置

跟着做

步骤 1:用 Bundler 创建 Gem 骨架

# 创建项目
bundle gem my_tool --test=rspec --ci=github
cd my_tool

# 生成的关键文件:
# my_tool.gemspec    # Gem 元数据
# lib/my_tool.rb     # 主入口
# exe/my_tool        # 可执行文件
# spec/              # RSpec 测试

编辑 my_tool.gemspec:

Gem::Specification.new do |spec|
  spec.name          = "my_tool"
  spec.version       = MyTool::VERSION
  spec.authors       = ["Your Name"]
  spec.email         = ["you@example.com"]

  spec.summary       = "文件批量重命名工具"
  spec.description   = "按规则批量重命名文件的 CLI 工具"
  spec.homepage      = "https://github.com/yourname/my_tool"
  spec.license       = "MIT"
  spec.required_ruby_version = ">= 3.0"

  spec.metadata["allowed_push_host"] = "https://rubygems.org"
  spec.metadata["homepage_uri"] = spec.homepage
  spec.metadata["source_code_uri"] = spec.homepage

  spec.files = Dir["lib/**/*", "exe/*", "README.md"]
  spec.bindir = "exe"
  spec.executables = ["my_tool"]
  spec.require_paths = ["lib"]

  spec.add_dependency "thor", "~> 1.3"   # CLI 框架
end

步骤 2:写 CLI 核心逻辑

lib/my_tool.rb:

# frozen_string_literal: true

require_relative "my_tool/version"
require_relative "my_tool/renamer"
require_relative "my_tool/cli"

module MyTool
  class Error < StandardError; end
end

lib/my_tool/version.rb:

# frozen_string_literal: true module MyTool VERSION = "0.1.0" end

lib/my_tool/renamer.rb:

# frozen_string_literal: true

module MyTool
  class Renamer
    def initialize(pattern:, replacement:, dry_run: false)
      @pattern = pattern
      @replacement = replacement
      @dry_run = dry_run
    end

    def rename_files(directory)
      files = Dir.glob(File.join(directory, "*"))
      renamed = []

      files.each do |file|
        next unless File.file?(file)

        basename = File.basename(file)
        new_name = basename.gsub(/#{@pattern}/, @replacement)

        next if new_name == basename

        new_path = File.join(directory, new_name)
        renamed << { from: file, to: new_path }

        unless @dry_run
          File.rename(file, new_path)
        end
      end

      renamed
    end
  end
end

lib/my_tool/cli.rb:

# frozen_string_literal: true

require "thor"

module MyTool
  class CLI < Thor
    desc "rename DIRECTORY", "批量重命名目录中的文件"
    option :pattern, type: :string, required: true, desc: "匹配模式(正则)"
    option :replacement, type: :string, required: true, desc: "替换内容"
    option :dry_run, type: :boolean, default: false, desc: "预览模式,不实际执行"

    def rename(directory)
      unless Dir.exist?(directory)
        puts "错误:目录 #{directory} 不存在"
        exit 1
      end

      renamer = Renamer.new(
        pattern: options[:pattern],
        replacement: options[:replacement],
        dry_run: options[:dry_run]
      )

      results = renamer.rename_files(directory)

      if results.empty?
        puts "没有匹配的文件"
      else
        mode = options[:dry_run] ? "[预览] " : ""
        results.each do |r|
          puts "#{mode}#{File.basename(r[:from])} → #{File.basename(r[:to])}"
        end
        puts "#{mode}共处理 #{results.size} 个文件"
      end
    rescue RegexpError => e
      puts "正则表达式错误:#{e.message}"
      exit 1
    end
  end
end

exe/my_tool(可执行文件):

#!/usr/bin/env ruby
# frozen_string_literal: true

require "my_tool"

MyTool::CLI.start(ARGV)

步骤 3:写 RSpec 测试

spec/my_tool/renamer_spec.rb:

# frozen_string_literal: true

require "tmpdir"
require "fileutils"

RSpec.describe MyTool::Renamer do
  let(:tmpdir) { Dir.mktmpdir }

  after do
    FileUtils.rm_rf(tmpdir)
  end

  describe "#rename_files" do
    it "按模式重命名文件" do
      File.write(File.join(tmpdir, "test_001.txt"), "content")
      File.write(File.join(tmpdir, "test_002.txt"), "content")

      renamer = MyTool::Renamer.new(pattern: "test", replacement: "prod")
      results = renamer.rename_files(tmpdir)

      expect(results.size).to eq(2)
      expect(File.exist?(File.join(tmpdir, "prod_001.txt"))).to be true
    end

    it "dry_run 模式不实际执行" do
      File.write(File.join(tmpdir, "test_001.txt"), "content")

      renamer = MyTool::Renamer.new(pattern: "test", replacement: "prod", dry_run: true)
      results = renamer.rename_files(tmpdir)

      expect(results.size).to eq(1)
      expect(File.exist?(File.join(tmpdir, "test_001.txt"))).to be true
      expect(File.exist?(File.join(tmpdir, "prod_001.txt"))).to be false
    end

    it "无匹配时返回空数组" do
      File.write(File.join(tmpdir, "hello.txt"), "content")

      renamer = MyTool::Renamer.new(pattern: "xyz", replacement: "abc")
      results = renamer.rename_files(tmpdir)

      expect(results).to be_empty
    end
  end
end

步骤 4:配置 RuboCop 与运行

# .rubocop.yml
AllCops:
  TargetRubyVersion: 3.0
  NewCops: enable
  Exclude:
    - "vendor/**/*"
    - "tmp/**/*"

Style/Documentation:
  Enabled: false

Metrics/MethodLength:
  Max: 20
# 运行测试
bundle exec rspec

# 运行 RuboCop
bundle exec rubocop

# 自动修复
bundle exec rubocop -a

# 本地安装测试
gem build my_tool.gemspec
gem install ./my_tool-0.1.0.gem

# 测试 CLI
my_tool rename /tmp/test --pattern=test --replacement=prod --dry-run

步骤 5:发布到 RubyGems(可选)

# 注册 rubygems.org 账号
gem signin

# 发布
rake release

# 或手动
gem build my_tool.gemspec
gem push my_tool-0.1.0.gem

讲清楚要点

  1. Bundler 的 bundle gem 是标准起点:它生成 gemspec、Gemfile、Rakefile、测试框架配置——比手动创建更规范。--test=rspec 选 RSpec,--ci=github 加 GitHub Actions 配置。
  2. lib/ 放库代码,exe/ 放可执行文件:lib/my_tool.rb 是主入口(require "my_tool" 加载);exe/my_tool 是终端命令。gemspec 的 spec.executables 声明可执行文件。
  3. Thor 是 CLI 框架的标准选择:比 Ruby 自带的 OptionParser 更声明式。desc 定义命令描述,option 定义参数,Thor 自动生成 --help。
  4. dry_run 模式是 CLI 工具的专业素养:文件操作前预览结果,避免误删。用 --dry-run 参数控制,内部逻辑只是跳过 File.rename。
  5. 测试用 Dir.mktmpdir 隔离:文件系统测试必须创建临时目录,测试后清理,避免污染真实环境。

你可能会懵的地方

  • gemspec 和 Gemfile 有什么区别? gemspec 定义「这个 Gem 本身是什么」(名称、版本、依赖、文件列表);Gemfile 定义「开发这个 Gem 需要什么」(测试框架、调试工具)。Gemfile 里 gemspec 一行把 gemspec 的依赖合并进来。

  • 为什么 lib/ 下要嵌套 my_tool/ 目录? Ruby 的 require 路径约定:lib/my_tool.rb 是入口,lib/my_tool/*.rb 是内部模块。这样 require "my_tool" 只加载入口,入口再 require_relative 加载子模块,避免命名冲突。

  • Thor 和 OptionParser 怎么选? Thor:多命令 CLI(如 rails new、rails server),声明式,自动生成 help;OptionParser:单命令、参数复杂、需要精细控制。新项目用 Thor。

  • Gem 的版本号怎么管理? lib/my_tool/version.rb 里定义 VERSION = "0.1.0",gemspec 引用它。发布时改版本号、打 git tag、rake release 一键发布。语义化版本:MAJOR.MINOR.PATCH。

  • 发布 Gem 到 rubygems.org 需要什么? 注册账号、gem signin 本地认证、确保 allowed_push_host 配置正确。发布后任何人 gem install my_tool 就能用。

错法对照

错法问题正解
gemspec 里 files 写 Dir["**/*"]包含测试、临时文件精确指定 Dir["lib/**/*", "exe/*"]
CLI 直接 exit 不返回码调用方无法判断成败exit 0 成功,exit 1 失败
文件操作不检查目录存在NoMethodError 或 Errno::ENOENTDir.exist? 前置检查
正则用户输入不处理特殊字符正则注入或意外匹配Regexp.escape 转义,或捕获 RegexpError
测试写真实文件不清理测试间互相污染Dir.mktmpdir + after { FileUtils.rm_rf }

卡点急救

  1. bundle exec ruby exe/my_tool 报 LoadError:lib 不在 load path。bundle exec ruby -Ilib exe/my_tool,或确保 gemspec 的 require_paths 包含 lib。
  2. Thor 参数解析失败:option :pattern, type: :string, required: true 确保必填参数有校验。用户没传时 Thor 自动报错。
  3. RSpec 测试里文件创建失败:Dir.mktmpdir 需要 require "tmpdir"。Windows 上路径分隔符用 File.join 兼容。

面试常问

  • Gem 的目录结构和关键文件? 标准结构:lib/(库代码)、exe/(可执行文件)、spec/ 或 test/(测试)、gemspec(元数据)、Gemfile(开发依赖)、Rakefile(任务)。Bundler 的 bundle gem 一键生成。

  • 如何发布一个 Gem 到 RubyGems?

    1. 写代码和测试;2. 配置 gemspec;3. gem build 打包;4. gem push 发布。rake release 自动化版本号、git tag、push 三步。
  • CLI 工具的错误处理最佳实践? 用户输入验证(文件存在、参数合法);清晰的错误信息(用户能看懂);正确的退出码(0 成功,非 0 失败);dry_run 模式预览危险操作;日志输出到 stderr,结果输出到 stdout。

自测三问

  1. bundle gem 生成的目录结构中,lib/ 和 exe/ 的分工是什么?
  2. gemspec 的 spec.files 和 spec.executables 分别控制什么?
  3. 为什么 CLI 工具要提供 --dry-run 模式?实现上怎么设计?

小结一句话

综合练习的核心是「把语法变成产品」——Bundler 搭骨架、Thor 做 CLI、RSpec 保质量、RuboCop 统一风格、RubyGems 发出去。

延伸

  • Bundler Gem 创建指南
  • Thor 官方文档
  • 恭喜!Ruby Essentials 全部完成。下一步:Rails 路线图或 Ruby 源码阅读。
加载本节练习…
← 上一课测试与质量