Perl 调外部命令有三式:反引号收 stdout、system 只要退出码、open 管道流式读。三式各配一种风险,配套的解法是列表形式传参(防注入)加超时防护(防卡死)。
# 一式:反引号,捕获标准输出 my $df = `df -h /data`; my @top = `top -b -n 1`; # 列表上下文按行收 # 二式:system,只要成败 my $rc = system('gzip', '/data/app.log'); $rc == 0 or warn "gzip 失败,退出码 $rc"; # 三式:open 管道,流式读大输出 open my $ph, '-|', 'tail', '-n', '100000', '/data/big.log' or die $!; while (my $line = $ph>) { parse($line); } close $ph;
第三式适合输出量大的命令:tail 百万行不占内存,Perl 端照旧逐行处理——把外部命令当文件句柄用,前五章的流水线经验全部平移过来。
system("tar czf $dst $src"); # 危险:$src 含空格或分号时出事 system('tar', 'czf', $dst, $src); # 安全:参数不经 shell 解释
文件名来自用户输入时,字符串形式等于把 shell 的钥匙递出去。列表形式绕过 shell,空格、通配符都只是普通字符。
system 没有超时参数,挂住的子进程会拖住整个脚本。方案是用 eval 加 alarm 信号:
eval { local $SIG{ALRM} = sub { die "超时\n" }; alarm 30; # 30 秒上限 system('curl', '-s', $url); alarm 0; # 完成,解除 }; warn "外部命令超时或失败: $@" if $@;

$$ 是本进程号,fork 复制进程做并行、waitpid 收尸,exec 把自己换成别的程序。日志脚本里更常用的是懒办法——Proc::Background 这类模块把 fork/wait 封装成对象,按需再学不迟。
⚠️ 常见坑:反引号里
$?才是退出码、$!不是。判断命令成败用$? == 0,拿 stderr 则要2>&1重定向进反引号。
把三式调用拼成一个真实的归档任务——压缩、校验、清理,每步的外部命令都按防护规范来:
my $log = '/data/app/2026-08-21.log'; my $arch = "$log.gz"; # 步骤一:压缩(列表传参 + 退出码检查) system('gzip', '-k', $log) == 0 or die "压缩失败,退出码 $? >> 8"; # 步骤二:校验完整性(反引号收输出) my $check = `zcat $arch | wc -l`; chomp(my $orig = `wc -l < $log`); die "行数不一致,归档不可信" unless $check == $orig; # 步骤三:确认后才清理原件(危险动作放最后) unlink $log or warn "原件删除失败: $!";
三个细节对应三条纪律:退出码判断用 $? >> 8(system 把实际退出码放在高八位);危险动作(删除)必须排在验证之后;任何一步失败都停在那里,绝不带病前进。这套顺序写死成模板,归档任务从此可以无人值守。
system 与反引号默认都拿不到 stderr,而排错信息恰恰常在 stderr。正式做法用 Capture::Tiny 一次分取三流:
use Capture::Tiny qw(capture); my ($stdout, $stderr, $exit) = capture { system('openssl', 'x509', '-in', $cert, '-noout', '-dates'); }; die "证书解析失败: $stderr" if $exit != 0; print "有效期信息: $stdout";
stdout 进解析、stderr 进报错、退出码做闸门,三条信息流各归各位——比 2>&1 混流后再人工拆分干净得多。诊断信息与数据混在同一管道里,是很多"输出解析莫名失败"的隐形根源。
外部调用在两个平台上行为不同,跨平台脚本要记三条。第一,shell 不同:列表形式 system 在两边都绕过 shell,这是最大公约数,优先使用;但反引号在 Windows 走 cmd、Unix 走 sh,命令名与引号规则都可能不同——跨平台脚本里反引号只用于两平台都一致的核心命令(如 dir 换成 ls 的问题恰恰说明不一致)。第二,换行符:子进程输出在 Windows 是 CRLF,逐行处理前统一 s/\r?\n$//,否则哈希键尾部藏着看不见的 \r。第三,路径分隔符:传给外部命令的路径用 File::Spec::Functions 的 catfile 拼接,别硬编码斜杠方向。三条合起来的心法:跨平台的外部调用,把"差异面"压缩到路径与换行两个点上集中处理,其余全部走列表形式与纯 Perl 实现,可移植性就不是玄学而是工程。检验成果的方式也简单:同一份脚本在两个平台各跑一遍测试集(第 7 章的资产),双绿才算跨平台成立——没跑过的"应该兼容",一律按不兼容对待。最后提一个高频的隐形差异收尾:退出码语义——两平台都遵循零即成功的惯例,但被信号杀死时 Unix 的 system 返回信号编号混在 $? 低八位,Windows 则固定返回特定错误码;统一用 system(...) == 0 判断成败、非零时原样记录 $?,就能同时兼容两种表达。
$? 判命令成败,别信 $!