Perl 11:空的末列不是不存在的列
日志最后一个消息为空时,原始行会以第三个制表符结束。如果只凭肉眼查看文件,...<TAB>api<TAB> 与缺少末列的 ...<TAB>api 很容易看混;解析器却不能把它们都解释成一条四字段记录。
LogTool::read_records 从原始字节按 LF 读一行,显式解码后只删除尾部的一个 \n,用 split /\t/, $line, -1 保留末尾空列,再检查恰好四项。demo.tsv 的第三行消息是合法空串,import 输出 IMPORTED<TAB>3<LF>;测试构造缺失第四列时,STDOUT 空,STDERR 为 line 1: expected four fields、退出码 2。额外的制表符也会触发同样拒绝。perlfunc 的 split 条目解释了 LIMIT 的效果,默认省略这个负 LIMIT 就不能直接依赖尾空列保持不变。
行终止符的决策也可复核:测试从合成的 ...<TAB>api<TAB> 输入去掉最后 LF,parse 仍接受并规范输出一个 LF;换成 CRLF,剩下的 CR 进入待校验记录,被明确拒绝,状态 2。这里没有擅自将所有平台换行格式视为相同。文档记录的契约是 LF 输入(末行 LF 可省略)、LF 输出;要增加 CRLF 支持,需要另写可逆的输入规范,而不是直接 s/\s+$//,否则消息末尾的空格也会丢失。
有界逐行读取还需要处理跨块的行
原定课程的五字段入口支持 LF、CRLF 和末行无终止符,旧四字段入口仍维持上面的拒绝 CRLF 规则。两个格式有各自金标;新版的支持不能反向改变旧版测试。新版 LogTool::Course::_lines 用 4,096 字节块读取,把跨块内容累积成一行,只移除该行终止符,不移除消息末尾空格。
一条超长输入不能先用 <$handle> 全部读到内存再检查。新版累积长度超过 65,536 字节后进入丢弃状态,只查找下一次 LF,不继续增加缓冲;随后记录超长行号,恢复读取下一条。整文件还受 16 MiB 限制,所有接受记录仍存入数组,因而不能承诺任意规模的流式处理。界限同时限制字段解析与后续 JSONL 工作量。
边界反例由 t/course-core.t 构造:一条 70,000 字节的坏行后接合法记录,拒绝数为一,接受数也为一。相邻的 LF、CRLF、最后无 LF 对照得到三条输入。空文件的输入数为零,单独空行却是一条不满足字段数的拒绝记录;两者不能混淆。
读取操作还要把 sysread 的未定义返回值与返回零分开。零表示到达 EOF;未定义表示失败,不能把读错误当作合法空文件。t/course-input-faults.t 在独立测试进程中注入读取和关闭错误,检查诊断并恢复标志后再导入同一份样本;这是受控故障,不是声称所有硬件读错误都曾实际发生。
[PATTERN] 有界读取的状态包括积累、超限丢弃、终止符恢复和 EOF;只定义一个最大数字,不能证明缓冲始终有界。
练习:合法消息是一个空格而不是空串时,能否在读行时 trim 整行?不能,它会改变用户消息的真实内容;若业务以后要求裁剪,必须对消息单独规定、变更金标。第二题:末行没有 LF,是否等于没有这条记录?不等于,非空剩余缓冲仍交给解析器。旧正反例、STDOUT/STDERR/退出码在 examples/perl-language/evidence/11-14.md,新版在补全证据。
| 输入边界 | 新版结果 |
|---|---|
| LF、CRLF | 移除终止符后解析 |
| 末行无 LF | 非空剩余内容仍是一行 |
| 空文件 | 零记录 |
| 超长行 | 记录拒绝并恢复下一行 |
| 读取失败 | 整批非零失败 |
参考已校验的 Perl 5.38.2 pod/perlfunc.pod(split、sysread、close)、pod/perldata.pod(列表与空字符串)。






