深入 Ruby 27:CLI、OptionParser 与退出码
一个命令有三个独立结果 taskbook stats 不只有屏幕上看到的文本。自动化调用者同时观察 stdout、stderr 和退出状态。如果输入失败却退出零,流水线会把失败当成成功;如果把提示语和 JSON 写进同一 stdout,下游解析器会拒绝原本正确的统计结果。 Taskbook 的命令合同因此明确为:成功数据写到 stdout,正常成功的 stderr 为空;参数或输入错误退出 2 且 stdout 为空,文件和 IO 错误退出 3。输出写入途中发生 IO 错误仍可能留下部分字节,调用者必须检查退出状态。帮助信息退出零。输出文件默认拒绝覆盖。它们是本工具的约定,不是所有 Unix 命令必须采用的统一数字规范。 本篇前置是资源、包装、测试与数据边界。入口为 examples/ruby/exe/taskbook,实测通过子进程完成,不能用“直接调用库函数通过”替代 CLI 验收。 参数解析只负责入口的语法 OptionParser 将命令行字符串转换成指定的选项值: 12345678910options = { format: 'json',...
深入 Ruby 26:JSON、CSV 与领域输入边界
系列导航 导读 · 上一篇:25:RBS、Steep 与静态检查边界 · 下一篇:27:CLI、OptionParser 与退出码 · 完整源码包 合法 JSON 仍然可能是非法任务 下面三段文本都是可以讨论语法结构的输入,但对 Taskbook 来说只有最后一种形状可能进入任务校验: 1{} 1[{"id": 1, "priority": "3"}] 12[{"id": 1, "title": "read Ruby", "priority": 3, "tags": ["ruby"], "status": "todo"}] 第一段顶层不是任务数组;第二段缺少字段,priority 还是字符串;第三段拥有约定字段与基本类型,之后仍要检查 ID 正数、标题非空、优先级范围、标签内容、状态枚举和集合内重复 I...
深入 Ruby 25:RBS、Steep 与静态检查边界
系列导航 导读 · 上一篇:24:调试器、警告与语法诊断 · 下一篇:26:JSON、CSV 与领域输入边界 · 完整源码包 签名承诺了整数,Ruby 会自动拒绝字符串吗 一个方法在 RBS 中声明返回 Integer,Ruby 执行器并不会因为旁边存在签名文件,就自动验证每次返回。RBS 描述接口,Steep 根据接口分析选定代码,运行时则继续遵循 Ruby 的动态语义。把这三层分开,才能理解“类型检查通过”和“程序实际正确”为什么需要分别验收。 本篇使用 RBS 3.9.5、Steep 1.10.0 和 CRuby 3.4.11。前置是鸭子类型、数据表示、依赖管理和行为测试。完整实验在 examples/ruby/labs/25/,签名在 sig/typed_tasks.rbs,检查范围由 Steepfile 明确指定。 为了让推断过程可见,实验选取 Taskbook 的任务集合与标题遍历协议,构成独立的 TypedTasks 子集。它不等于整个 Taskbook 已通过类型检查;JSON/CSV、CLI、HTTP 和动态实验仍由各自行为测试验证。 record 把需要的字段...
深入 Ruby 24:调试器、警告与语法诊断
系列导航 导读 · 上一篇:23:Minitest 与可失败的行为测试 · 下一篇:25:RBS、Steep 与静态检查边界 · 完整源码包 停在计算现场,检查当前帧 统计结果不符合预期时,第一步是检查输入和中间值,而不是立即修改算法。一个断点可以把“这个变量应该是二”变成对实际运行帧的观察: 1234567def report_total(tasks) total = tasks.length binding.break totalendputs report_total([:one, :two]) 用固定 debug 版本启动该文件,执行 p total 和 bt,就能分别看到局部变量与调用栈。继续执行后,程序正常输出结果。断点位置在赋值之后,因此局部变量已经建立;把断点移到赋值之前,就不能用同一个预期解释观察结果。 本篇前置是方法查找、异常和行为测试。冻结依赖为 debug 1.11.0,完整被调试文件在 examples/ruby/labs/24/probe.rb,自动验证入口为 labs/24/run.rb。本例只使用本地终端输入,不启动远端调试端口。 调试命令属...
深入 Ruby 23:Minitest 与可失败的行为测试
系列导航 导读 · 上一篇:22:gemspec 与可安装的交付边界 · 下一篇:24:调试器、警告与语法诊断 · 完整源码包 测试一直通过,可能是没有观察到错误 一个统计函数应该报告任务数量。如果把实现改成永远返回零,测试仍然通过,就说明测试没有约束这个公开行为。测试文件存在、运行命令退出零、打印绿色结果,都不能替代“错误实现确实会被识别”这个问题。 第 23 篇把已有断言组织进 Minitest,并给测试入口设置一个具体的负控制:在临时子进程中把 Taskbook.summary 替换成错误版本,随后运行同一组行为测试,要求它返回失败。原工程实现保持不变,错误只存在于该子进程。 前置是异常、资源管理、包与入口。测试使用固定的 Minitest 5.25.5;项目入口是 test/engineering_boundaries_test.rb,实验调度脚本是 labs/23/run.rb。官网可能已经展示更新版本,运行证据以工程锁定版本为准。 从脚本断言过渡到独立场景 最早的实验使用 raise unless condition。这种写法依赖少,适合解释单个语言机制;场景增多后,...
深入 Ruby 22:gemspec 与可安装的交付边界
系列导航 导读 · 上一篇:21:RubyGems、Bundler 与版本激活 · 下一篇:23:Minitest 与可失败的行为测试 · 完整源码包 源码目录能运行,为什么安装后却失败 ruby -Ilib exe/taskbook stats 在仓库里成功,只说明源码布局符合这次启动方式。打包漏掉一个文件、可执行入口没有声明、运行依赖只写在开发环境,都会让安装后的程序失败。 可靠的包装实验需要离开源码目录。在临时位置安装生成的 gem,然后从另一个临时工作目录分别加载库和启动 CLI。这样才能排除 -Ilib 或当前目录意外提供文件的情况。完整实验是 examples/ruby/labs/22/run.rb,前置为对象接口、代码加载和 Bundler。 Taskbook 使用包名 taskbook-workshop,文件仍叫 taskbook.gemspec,加载入口是 require 'taskbook',命令名是 taskbook。这几个名字服务不同层次,不要求全部相同。实际发布前还要确认公共名称的使用权;本系列只生成和安装本地包,没有远端发布。 gemspec 是包的声...
深入 Ruby 21:RubyGems、Bundler 与版本激活
系列导航 导读 · 上一篇:20:require、load 与 autoload · 下一篇:22:gemspec 与可安装的交付边界 · 完整源码包 安装过一个版本,不代表进程使用它 同一台机器可以保存同一个 gem 的多个版本。文件存在于磁盘,当前进程允许选择哪些版本,以及 require 最后执行哪个入口,是三个不同状态。排查依赖问题时,如果只提供 gem list,只能证明可见安装集合,不能证明故障进程的选择结果。 Taskbook 的版本实验直接读取当前进程已经激活的 specification: 12345678910111213require 'bundler/setup'require 'json'require 'rack'raise unless Gem.loaded_specs.fetch('json').version.to_s == '2.13.2'raise unless Gem.loaded_specs.fetch('rack')....
深入 Ruby 20:require、load 与 autoload
一个文件执行了几次 在文件顶层增加计数器,连续加载四次,不一定得到四: 1234567891011121314require 'tmpdir'Dir.mktmpdir do |dir| path = File.join(dir, 'feature.rb') File.write(path, '$count = ($count || 0) + 1') raise unless require(path) == true raise unless require(path) == false raise unless $count == 1 load path load path raise unless $count == 3end 第一次 require 执行文件,第二次返回 false。两次 load 都执行文件,因此计数器最后是 3。这个实验把“找到文件”“执行文件”“记录已经加载”拆开了。只有分清这几件事,才能解释为什么修改文件后重新 require 没变化,或为什么重复 load 会重复注册回调。...
深入 Ruby 19:文件、IO 与路径的资源边界
返回字符串之后,文件还开着吗 下面两个函数都能读出相同内容,却把资源责任交给了不同的地方: 1234567def read_owned(path) File.open(path, 'rb') { |file| file.read }enddef read_borrowed(stream) stream.readend 第一个函数创建流,block 结束时关闭流;第二个函数接收调用者已经拥有的流,读取结束后仍由调用者决定是否关闭。把第二个函数也写成“读完就 close”,会破坏调用者接着读取其他记录的计划。IO 接口设计首先要回答谁拥有资源,才有可能正确处理异常与关闭。 前置是第 03 篇的编码和第 18 篇的异常传播。完整实验在 examples/ruby/labs/19/run.rb,使用临时目录、普通本地文件和 CRuby 3.4.11。文件系统的掉电恢复和多进程写入协调没有被这组实验覆盖。 block 把生命周期限制在调用范围 保留流对象引用,可以在异常之后检查是否真的关闭: 123456789101112131415161718...
深入 Ruby 18:异常、ensure 与 throw/catch
清理完成,为什么失败却消失了 任务导入函数打开文件、解析记录,最后释放资源。给函数加上 ensure 看似只改变了清理过程,下面的写法却改变了调用者看到的结果: 1234567def import raise ArgumentError, 'invalid priority'ensure return :closedendraise unless import == :closed ArgumentError 没有到达调用者。清理子句里的显式 return 产生新的返回动作,取代了原本向外传播的异常。这个例子说明,审阅异常处理不能只问“有没有执行清理”,还要追踪离开当前方法时携带的是正常值、异常,还是其他控制转移。 本篇前置是方法返回、block 的非局部控制流和对象调用。实验使用 CRuby 3.4.11,完整脚本在 examples/ruby/labs/18/run.rb。核心问题限于同一线程内的栈展开;线程异步异常与取消策略属于后续并发章节。 一个表达式的四条出口 把各子句记录成事件,比只打印一句“捕获成功”更容易检查顺序: 123456789101...

