浅谈 awk 中管道的用法
把“文本”变成“进程”,再把“进程”变回“文本”,只用一条 awk 语句。
1 为什么需要管道
awk 本身是一门面向“行”的语言,默认逐行读取输入。
但在实际脚本里,我们经常需要:
- 把当前行送给外部命令(rev、sort、grep、sqlite …)
- 读取外部命令的输出做后续计算
- 与系统工具链协同,而不用离开 awk 进程
这些需求靠“管道”完成,awk 提供了两条路:
a) 隐式管道(print | cmd
/ cmd | getline
)
b) 显式 system()(一次性执行)
两者对比如下表:
需求 | 用 system() | 用隐式管道 |
---|---|---|
启动一次外部命令 | ✅ | ✅ |
持续读写数据流 | ❌ | ✅ |
避免多余 /bin/sh | ❌ | ✅ |
及时 flush/close | 手动 | 自动+手动 |
不难看出,隐式管道更轻量、更可控,这也是本文的重点。
2 awk 管道的三种语法形态
以下语法基于 POSIX 标准,适用于主流 awk 实现(gawk、mawk、nawk)。
2.1 输出型管道:把 awk 的数据喂给外部进程
print [items] | "shell-cmd"
printf fmt, items | "shell-cmd"
2.2 输入型管道:把外部进程的输出读进 awk
"shell-cmd" | getline # 读到 $0, NF, NR 被更新
"shell-cmd" | getline var # 只读到变量 var
2.3 双工型:同时读写(高级用法,gawk 扩展)
"coprocess" |& print ... # 写到协程
"coprocess" |& getline ... # 从协程读
(本文不展开)
对比
形式 | 流向 | 说明 |
---|---|---|
`print items | “shell-cmd”` | awk → 外部 |
`“shell-cmd” | getline` | 外部 → awk |
`“shell-cmd” | getline var` | 外部 → awk |
`“coproc” | & getline` | 双向 |
3 实战 10 连击
以下是 10 个精心设计的实战案例,覆盖从简单到复杂的场景,展示 awk 管道的灵活性和强大功能。
3.1 奇偶行反转
awk 'NR%2 {print | "rev"; close("rev"); next} 1' file
说明:奇数行通过 rev
反转字符顺序,偶数行保持不变。close("rev")
确保每次调用后关闭管道,避免资源泄漏。
3.2 实时 DNS 解析
awk '{ cmd="dig +short " $1; cmd | getline ip; close(cmd); print $1, ip }' access.log
说明:从 access.log
中提取域名,通过 dig
获取 IP 地址并输出。close(cmd)
防止管道堆积。
3.3 每 1000 行排序一次
{buf = buf $0 "\n"
}
NR % 1000 == 0 {print buf | "sort"close("sort")buf = ""
}
END {if (buf) { print buf | "sort"; close("sort") }
}
说明:累积 1000 行数据后通过 sort
排序输出,END
块处理剩余不足 1000 行的数据。
3.4 HMAC-SHA256 每行计算
awk -v k='secret' '{cmd = "openssl dgst -sha256 -hmac " kprintf "%s", $0 | cmdclose(cmd)cmd | getline hclose(cmd)print $0 " -> " h
}' input.txt
说明:为每行文本计算 HMAC-SHA256 签名,适合校验数据完整性。
3.5 并行 8 路任务
BEGIN {for (i=1; i<=8; i++) print i | "xargs -P8 -I{} sh -c '\''sleep {}; echo {} done'\''"
}
说明:启动 8 个并行任务,模拟并发处理场景,适合批量任务调度。
3.6 持续读取 ps 输出
BEGIN {while ("ps -eo pid,pcpu --no-headers" | getline) cpu[$1] = $2for (pid in cpu) print pid, cpu[pid]
}
说明:实时监控进程的 CPU 使用率,存储在数组中并输出。
3.7 并发 curl 下载
{cmd = "curl -s " $1cmd | getline bodyclose(cmd)print $1, length(body)
}' urls.txt
说明:从 urls.txt
读取 URL,逐行调用 curl
下载内容并输出长度。
3.8 AWK 作为 Kafka Producer
awk '{ print $0 | "kcat -b broker:9092 -t topic" }' app.log
说明:将日志实时发送到 Kafka 的指定主题,适合日志流处理。
3.9 交互式 bc 计算器
BEGIN {cmd = "bc -l"for (i=1; i<=5; i++) {print i "/3" | cmdcmd | getline ansprint i, "÷ 3 =", ans}close(cmd)
}
说明:与 bc
交互,计算 1 到 5 除以 3 的结果,展示管道的双向交互能力。
3.10 动态 JSON 解析与统计
{cmd = "jq -c '{ts: .timestamp, code: .status}'"print $0 | cmdcmd | getline jsonclose(cmd)print json
}' app.log
说明:将日志行通过 jq
解析为 JSON 格式,提取 timestamp
和 status
字段,适合实时日志处理。
4 getline 返回值与错误处理
getline
的返回值需特别注意:
返回值 | 含义 |
---|---|
1 | 成功读取一行 |
0 | 文件末尾(EOF) |
-1 | 系统错误(如命令失败) |
-2 | 中断(如管道被关闭) |
稳健模板:
if (("cmd" | getline var) <= 0) {print "Error: Failed to execute cmd" > "/dev/stderr"next
}
优化建议:
- 总是检查
getline
返回值,避免因命令失败导致逻辑错误。 - 使用
close(cmd)
及时关闭管道,释放资源。
5 Shell 元字符逃逸
外部命令中可能包含危险字符,需妥善处理:
字符 | 风险 | 解决方法 |
---|---|---|
空格 | 参数拆分 | "\"" $0 "\"" |
\ , $ , " | 被 shell 解析 | sprintf("%q", $0) |
NULL 字节 | 命令截断 | 使用 base64 编码 |
安全示例:在 /etc/passwd
中搜索用户输入:
cmd = "grep -F -- " sprintf("%q", $0) " /etc/passwd"
cmd | getline result
close(cmd)
print result
6 性能与死锁
6.1 常见问题与解决
问题 | 现象 | 解决方法 |
---|---|---|
Fork 风暴 | 过多子进程耗尽 ulimit | 批量处理后统一 close() |
缓冲死锁 | 脚本挂起 | 使用 fflush("") 或 close() |
行缓冲延迟 | 实时性差 | 使用 stdbuf -oL cmd 或 unbuffer |
6.2 优化技巧
- 减少管道创建:缓存多行数据后一次性送往外部命令(如案例 3.3)。
- 异步处理:对于高并发场景,使用
xargs -P
或后台进程(cmd &
)。 - 缓冲管理:显式调用
fflush("")
确保数据及时送达外部命令。
示例:优化高频管道调用
{buf = buf $0 "\n"if (++count % 100 == 0) {print buf | "grep pattern"close("grep pattern")buf = ""}
}
END {if (buf) { print buf | "grep pattern"; close("grep pattern") }
}
7 底层实现(gawk 9.x)
了解 awk 管道的底层机制有助于调试和优化:
- 解析阶段:
print | "cmd"
解析为N_POPEN
节点。"cmd" | getline
解析为N_GETLINE
节点。
- 运行阶段:
- 调用
popen("cmd", mode)
,触发fork()
和pipe()
,最终通过execve("/bin/sh", "-c", "cmd")
执行命令。
- 调用
- 资源回收:
pclose(fd)
调用waitpid()
,清理子进程并移除 IO 表条目。
- 双工管道(coproc):
- 使用
socketpair()
和dup2()
实现双向通信。
- 使用
9 总结
awk 里的字符串不是命令,但一旦出现在 | "string"
或 "string" | getline
右侧,就立刻被提升为独立子进程的标准输入/输出。
掌握这条规则,你就能用几十个字节写出别人几十行脚本才能完成的流水线。