目录遍历靠 glob 或 opendir/readdir,文件甄别靠一元测试运算符:
-f是普通文件、-M是年龄(天)。两者组合就能回答"这个目录下最近一天修改过的 .log 文件有哪些"。
# 方式一:glob,模式匹配的快捷门 for my $f (glob '/var/log/app/*.log') { print "$f\n"; } # 方式二:opendir/readdir,能看见隐藏文件,拼路径更明确 opendir my $dh, '/var/log/app' or die "打不开目录: $!"; while (my $name = readdir $dh) { next if $name eq '.' or $name eq '..'; my $path = "/var/log/app/$name"; next unless -f $path; print "$path\n"; } closedir $dh;
日常取文件 glob 够用;要区分文件与子目录、或要递归时,opendir 的控制力更强。递归下钻推荐直接用 CPAN 的 File::Find:
use File::Find; find(sub { return unless /\.log$/; print "$File::Find::name\n"; }, '/var/log/app');
| 运算符 | 问的问题 | 日志场景 |
|---|---|---|
-e |
存在吗 | 处理前确认文件就位 |
-f / -d |
文件 / 目录 | 遍历时分流 |
-M |
距修改多少天 | 只处理今天的日志 |
-s |
多少字节 | 跳过空文件 |
-r / -w |
可读 / 可写 | 动手前验权限 |
for my $f (glob "$dir/*.log") { next unless -s $f; # 空文件跳过 next if -M $f > 1; # 只碰一天内的 process($f); }
mkdir $outdir or die "建目录失败: $!"; # 单层 make_path $deep_dir if !-d $deep_dir; # 多层,用 File::Path rename $tmp_report, $report or die "改名失败: $!"; # 原子上桌 unlink @stale_tmp; # 删一批临时文件
rename 在同一文件系统上通常是原子操作——这个性质是 5.3 节"临时文件 + 改名"惯例的基石:读报告的一方永远不会看到写了一半的文件。
💡 关键直觉:把"找文件"和"处理文件"分成两步写。先收集全部路径进数组,再统一处理——报错时能打印"第 17 个文件失败",比边找边处理的可排查性好一个量级。
把遍历、测试、统计拼成一段可直接抄走的体检代码,输出目录下每个日志的年龄与大小:
use File::Find; my ($n, $bytes, @stale) = (0, 0); find(sub { return unless -f $_ && /\.log(?:\.gz)?$/; $n++; $bytes += -s $_; push @stale, $File::Find::name if -M _ > 7; # _ 复用上次的 stat 缓存 }, $dir); printf "%d 个日志,共 %.1f MB\n", $n, $bytes / 1048576; printf "超过 7 天未动的有 %d 个:\n", scalar @stale; print " $_\n" for @stale;
第 4 行的 -M _ 有个实用细节:下划线表示"沿用上一次 stat 的结果",避免 -f 与 -s、-M 各查一次文件系统——大目录里这一下能省出可观的系统调用。这份体检单放进 cron 每天跑一次,日志堆积问题在变成事故前就能被看见。
批量操作文件还常撞上权限现实:一是 unlink 失败不置脚本于死地,它返回成功删除的个数,unlink @files or warn "有文件没删掉" 的写法比 die 合适,删不掉的旧归档不该阻断流水线;二是 chmod 要给新产出的报告文件收紧权限(含敏感数据时 chmod 0600, $report),默认权限往往偏宽;三是跨文件系统 rename 不再原子,会退化为"复制 + 删除",大文件上耗时且中途可见——重要报告产出目录应与临时目录同盘。
把本章知识串进一个真实形态的故障。现象:日志清理脚本每周日跑,某次执行后监控目录空了三小时。复盘时间线:脚本用 glob 拿文件列表,当夜日志轮转正在进行,glob 结果里混进了一个刚建出的临时目录(匹配了宽松模式);unlink 对目录无效并静默失败;但脚本随后按"清理失败则移动到回收目录"的逻辑执行 rename,把半成品目录搬走了,轮转进程找不到自己的临时目录报错退出,直到下轮重试才恢复。三层修复各自对应一条本章原则:glob 模式收紧并加 -f 过滤(只碰确认过的普通文件);rename 前同样测试(-f $src or next);清理脚本与轮转任务错峰(cron 分钟位错开)。教训浓缩成一句:批量操作对"列表里是什么"的信任必须为零——glob 给的是"匹配结果",不是"你要的东西",两者之间的差距就是事故的生存空间。把这个原则再推广半步就覆盖了日常:任何"拿一个列表去删/移/改"的代码,都值得在动作前打印列表人工确认一次(dry-run),确认无误再放开执行——五行 dry-run 代码,是批处理脚本最便宜的保险。dry-run 的实现形态通常是一个 --dry 命令行开关:有它就只打印"将要对哪些文件做什么",没有它才真动手,一行参数解析换一个"看清再动手"的机会,这笔账怎么算都划算。
File::Find-M、-s、-f 是日志脚本的三件常用测试