Perl 20:字段分隔、固定偏移和模式捕获各管什么
本系列的日志有三个 TAB 分隔符,第 4 列可以包含普通空格。用 split /\t/, $line, -1 得到四个字段;改成 split /\s+/, $line,消息 queued again 会再拆成两项,实测变成五个字段。正则捕获适合 16 的受限消息子语法,却不能替代整行的字段契约;substr 按固定位置取时间虽然短,也得先证明源格式确实固定且有足够长度。 perl examples/perl-language/experiments/16-20-patterns.pl 20 输出 tab-fields=4、whitespace-fields=5、fixed-prefix=short,STDERR 空、退出码 0。末行来自 substr('short', 0, 20):短输入并不会自动报“时间字段不够长”,却能返回 short。所以不能把无异常的 substr 当格式校验。正式工具先 split 成四列,再分别验证时间、等级和服务值;消息原样保留,没有通用 \s+ 裁剪。 123整行 TSV —— 明确 TAB / 保留末空 ——> 四个字段时间字段 —— ...
Perl 19A:闭包保存查询条件,而不是共享一份临时全局状态
按等级、耗时和消息片段筛日志时,最容易犯的错误是把“当前筛选条件”放进包变量。第一次查询设置 $level = 'ERROR',第二次查询设置 $level = 'WARN',若两个过滤器读的是同一个变量,先创建的过滤器也会跟着变。闭包适合这个场景:构造阶段把已经校验过的条件复制到词法变量,返回的代码引用以后只读取这些词法变量。 LogTool::Course::Filter::make_filter(%conditions) 支持三个条件:level 必须是 INFO、WARN 或 ERROR;min_duration_ms 是非负整数,和记录里的 duration_ms 比较;contains 是消息里的字面字符串,不当作正则。实现先复制 %conditions 里的值,再返回 sub { ... }。返回的闭包接收一条记录哈希引用,满足所有条件时返回真,否则返回假。这里保存的是条件值,不保存调用者传入的哈希容器。 运行 perl examples/perl-language/experiments/course-semantics.pl closure,STDOUT 为: ...
Perl 19:一次“跑得慢”不是复杂度证明
(a+)+ 把“每段由几个 a 组成”和“有几段”交给相邻的两个重复决定。长串 a 后再接一个不匹配的 X,正则不能成功,却有多种切分路线。若把 n 个 a 的 n−1 个空隙分别选为“分段”或“不分段”,数学上存在 2^(n−1) 种正整数分段法;这解释了表达式为何含有组合歧义,不等于证明当前 Perl 引擎真的逐一遍历了全部分段。官方 perlre 的 Backtracking/Atomic Grouping 一节直接讨论类似的 (.+)+ 无匹配模式及原子分组的取舍。 实验仅跑固定长度 6、8、10,再追加 X;命令 perl examples/perl-language/experiments/16-20-patterns.pl 19 的 STDOUT 是 size=6:partitions=32:rejected=1、size=8:partitions=128:rejected=1、size=10:partitions=512:rejected=1,STDERR 空、退出码 0。输出中的 partitions 是上述组合式的计算值,不是引擎运行时间、匹配尝试次数或经过剖...
Perl 18:用户提供的点号,不该变成任意字符
假设 CLI 以后按消息中的字面字符串查找,目标是 a.b。如果把它直接插入正则 /$needle/,点号成为“任意字符”,在 aXb a.b 中会先找到错误的 aXb。这是动态模式与用户字面数据混用的缺陷;不需要恶意生产日志,用两段固定合成字符串就能复现。 perl examples/perl-language/experiments/16-20-patterns.pl 18 的 STDOUT 是 unquoted=aXb、literal=a.b;STDERR 空、退出码 0。后者使用 /\Q$needle\E/,把插入部分按字面值处理;等价的准备方式可以使用 quotemeta。这里故意不从用户输入编译任意正则,也不承诺“套上 \Q 就解决整个输入安全问题”:解码、输入长度、字节/字符语义和整体过滤策略仍需各自约束。 旧 logtool.pl query 只接受枚举等级;当前 Course 入口已经新增 --contains,但它仍是字面包含,不是自由正则过滤参数。这也是一个可操作的设计判断:只需要精确包含时,不必先把问题变成运行时正则编译。若日后确需按字段模式筛选,须说明哪...
Perl 17:/g 的下一次匹配有位置状态
在字符串 a,b 上写 while ($text =~ /([^,]+)/g),第一次得到 a,第二次得到 b。差别并非正则凭空“记住上一条日志”,而是目标标量的搜索位置在匹配时变化。实测 pos($text) 先为 1、再为 3;下一次找不到时,默认失败会把位置重置为 undef。若混入 /gc,失败后不重置的契约又不同;不能把两种情况笼统说成“/g 永远继续”。 零长度匹配更容易把位置误判成终止条件。实验对 ab 用 /(?=.)/g:STDOUT 的 zero-width=0,1,而不是无穷多个 0;代码另设最多 3 次的护栏,防止未来改动变成挂起。这只验证了该模式和本机 Perl 5.38.2 的有限样本;文档对重复零长度匹配有专门规则,不能由一个 while 循环推论每个含 \G 的模式都安全。 执行 perl examples/perl-language/experiments/16-20-patterns.pl 17,四行输出和空 STDERR、退出码 0 记录在 examples/perl-language/evidence/15-20.md。工具本体目前按 TA...
Perl 16:只在本次匹配成功的分支使用捕获
消息 id=abc-01 能拆成 id 和 abc-01;消息 id= 不能变成一条没有值却被接受的记录。正则既要覆盖整个字段,又必须让后续代码只在匹配成功的分支使用捕获结果。用一个成功匹配后的 $1 再去检查失败匹配,是把上一次状态误当成这一次数据。 实验 perl examples/perl-language/experiments/16-20-patterns.pl 16 运行锚定在 \A、\z 的 (?<key>[a-z]+)=(?<value>[a-z0-9-]+):STDOUT 是 accepted:id:abc-01 与 rejected:id= 两行,STDERR 空,退出码 0。代码仅在 if ($message =~ /.../) 成功时读取 $+{key}、$+{value}。这是一段消息子语法的实验,不是说整行 TSV 都该换成一条大正则。日志字段数、时间和等级仍遵守之前的解析契约。 图:消息子语法的匹配与捕获分支 123原始合成行 -> 固定四列 -> message 字符串 ...
Perl 15:查询不到记录,不等于输入无效
Course 合成输入先经 import 变成 accepted/rejected 两份 JSON Lines。查询只读 accepted:不带条件的 query 输出全部四条;--level WARN 只输出 WARN;--min-duration 0 输出 duration 为数字且大于等于 0 的三条,排除 duration_ms:null;--contains '.*' 只匹配消息里真的含有点星两个字符的 ERROR 记录。查询条件为空、查询结果为空、输入无效,是三个不同状态。 examples/perl-language/bin/logtool 的查询路径先用 read_jsonl($input) 校验所有 JSON Lines,再构造 make_filter(%conditions)。level 只能是 INFO/WARN/ERROR;min-duration 必须是无前导零的非负整数;contains 是字面包含,不当作正则。tools/replay.pl --case all 覆盖 query-all、query-zero、literal-pattern、inva...
Perl 14:稳定报表不能依赖哈希遍历顺序
四条已接受 Course 记录的等级分别包含 INFO、WARN、INFO、ERROR。计数放进哈希很自然,但键的遍历顺序不是报表协议。perldata 的 hash initializer 提醒:写入顺序不保证取出顺序。依赖 keys %counts 偶然排列出来的一次输出,会在另一个哈希种子、另一个进程或代码变更后使同一份数据产生不同报表。 当前 Course 入口不再走旧 LogTool::report,而是在 examples/perl-language/bin/logtool 的 report 命令中创建 LogTool::Course::Aggregator,调用 $aggregator->rows 取已经排序的行。tools/replay.pl --case all 对 fixtures/course.tsv 的完整链路断言报表字节必须等于: 1234TOTAL 4ERROR 1INFO 2WARN 1 这个顺序与输入顺序无关:输入第一条是 INFO,第二条是 WARN,第四条是 ERROR;报表仍按等级名排序。course-engineering.t 还断言...
Perl 13:规范化服务名,也要拒绝假的闰日
仅靠 \d{4}-\d\d-\d\d 可以放行 2025-02-29。它长得像 UTC 时间,但 2025 不是闰年。格式合法与日期存在是两项检查;目前输入强制尾部 Z,所以即使日期有效也不表示日志时间由可信时钟生成。 read_records 用 Time::Local::timegm_modern 按 UTC 校验年、月、日、时、分、秒,年限制为 1970–2099;内部异常在行边界收束为 line N: invalid record。fixtures/bad-date.tsv 含 2025-02-29T00:00:00Z,CLI STDOUT 空、STDERR line 1: invalid record、退出码 2。对应的真实闰日 2024-02-29T00:00:00Z 则通过,绝不凭“正则看起来匹配”判日期正确。本机 Time::Local 是 1.30,属已安装核心模块;模块装载列表见 examples/perl-language/VERSIONS.md,没有发生第三方包隔离安装。 同一次规范化还处理 ASCII 服务名:输入 API 被接受,parse 输出 ap...
Perl 12:解析失败不能变成导入了前几条
接收三条日志时,如果第二条是 DEBUG 而等级集合只允许 INFO/WARN/ERROR,打印“已导入一条”会制造部分成功的错觉。旧四字段入口将每个文件作为一次读入事务:按行计数和验证,任一条不满足字段语法就报错;没有结果对象返回给 CLI,也不写出已解析的前缀。 当前语法是四个 UTF-8 字段。时间形状为固定 YYYY-MM-DDTHH:MM:SSZ;等级取三个大写词;服务名仅允许 ASCII 字母开头、后续字母/数字/下划线/连字符;消息可以空,却不能带 TAB、CR 或 LF。read_records 在读取时先查字段数,再查取值范围,最后才构造四键哈希引用。fixtures/invalid.tsv 的 DEBUG 被拒绝:import STDOUT 空、STDERR line 1: invalid record、退出码 2;同样不能把多余制表符静默吞进消息字段。 为什么把报错中的行号当接口?原始合成文件可以由两条有效记录加上一条错误记录拼出;行号从一次 read_records 调用内部的 my $line_number 计起,不能在同一个进程处理第二个文件时继续累计...










