查 demo.tsv 里的 ERROR 等级没有匹配,是完全有效的结果:退出 0、STDOUT 和 STDERR 均 0 字节。把输入改为 invalid.tsv,查询同样没有任何记录可发布,却必须是退出 2、STDOUT 空、STDERR line 1: invalid record\n。如果调用者只用 defined($output) 或只看文件长度,两个场景无法区分。

当前 CLI 将 open 失败、读失败、坏记录、子进程状态分别生成文字诊断,通过最外层 eval 统一映射为 CLI 状态 2;read_records 抛错时既不返回部分记录,也不输出部分查询。这个 2 是本工具的应用层约定,不等于 Perl 的 $!、子进程 $? 或操作系统 errno。报错中行号只在解析阶段有意义,不能把目录的读失败硬说成“第 1 行无效”。

图:空查询、格式错误与读取错误的三通道结果。

1
2
3
正常读完 + 0 条匹配 -> status 0, stdout "", stderr ""
字段损坏 -> status 2, stdout "", stderr "line 1: invalid record\n"
读错误 -> status 2, stdout "", stderr "cannot read input: ...\n"

练习:输入只含空文件时,import 应返回什么?数据契约允许 0 行,须输出 IMPORTED\t0\n、退出 0;不能因为同样没有记录就报 I/O 错。详细三通道正反例见 examples/perl-language/evidence/26-31.md,可重跑 t/26-31-objects-and-process.t;参考 Perl 5.38.2 pod/perlvar.pod 的 $!/$@ 语义与 pod/perlfunc.pod 的 eval。

把普通拒绝与批次异常分开

五字段 import 增加了普通拒绝结果。五行合成输入有四行合法、一行日历日期错误,成功输出 INPUT 5、ACCEPTED 4、REJECTED 1;拒绝 JSONL 保存原行、行号和原因。query 不再重新接受原始 TSV,它读取已验证 JSONL。没有匹配是空数组,序列化后可以是空 stdout,退出 0。

非法 UTF-8 与读取失败采用异常边界。无法可靠解码时,后续字段位置不再可信;读取失败时,输入是否完整不再可信。CLI 最外层 eval 捕获异常并输出 STDERR,退出 2。不能把 undef、空数组引用、异常文本和空字节流压成一个“失败值”,否则调用者无法判断能否继续。

块 eval 的最后表达式可能本身是假值。判断是否抛错时,常用 eval { operation(); 1 },成功显式返回真,再检查异常。$@ 是当前 eval 的动态错误槽,随后另一次成功 eval 会清空它。应立即把主错误复制到词法变量,再执行可能抛错的清理。如果清理也失败,将两者分别保存,不能让清理异常替换主异常。

Report.pm 的故障对照故意让 report close、临时文件 unlink、锁 close 连续失败。course-engineering.t 验证最终诊断仍含主关闭错误和两个附加清理错误。异常的文字来源因此可解释;“捕获过一次异常”本身不证明它经过嵌套清理后仍然保存。

[PATTERN] 函数接口先约定普通空结果和拒绝结果,再决定哪些失败必须抛异常。异常进入 CLI 后才映射应用退出码;模块不在内部直接 exit,以便测试和上层程序观察原始错误。

练习:先复制 $@,随后清理 eval 成功,是否还应报主失败?应当。主操作已经失败,清理成功只表示释放资源成功。若 report 最终关闭失败,旧报表必须保持;若 import 格式坏行进入拒绝清单,计数仍应守恒。两个分支在 tools/replay.pl 中分别验收。

来源:Perl 5.38.2 eval、$@、die。