服务器上最高频的编程,往往不是写服务,而是把文本变成答案:日志、CSV、配置、进程列表。Shell、awk、sed 常被揉成「三种脚本语言」——更准的说法是三种职责。
一句话:Shell 管流程,sed 改行,awk 算列;角色分清,命令行才不会变成正则迷宫。
三者到底是什么
| 工具 | 核心角色 | 最适合 |
|---|---|---|
| Shell(bash/zsh) | 命令编排器 | 变量、条件、循环、启动程序、重定向 |
| sed | 流式文本编辑器 | 替换、删除、选取、简单逐行变换 |
| awk | 面向记录与字段的文本处理语言 | 分列、聚合、统计、格式化报表 |
Shell 像导演,负责“先做什么、失败怎么办”;sed 像校对,逐行修改;awk 像表格分析师,把每行当记录、每列当字段。
管道:把程序接成流水线
Unix 工具通常只做好一件事,并通过三条通道组合:
- 标准输入 stdin:程序读取的数据
- 标准输出 stdout:正常结果
- 标准错误 stderr:诊断信息
1 | grep 'ERROR' app.log | sed 's/password=[^ ]*/password=***/g' | head |
数据从左向右流动:先筛错误行,再脱敏,最后只显示前十行。管道传递的是 stdout,默认不会传 stderr:
1 | command 2>error.log | another |
每一级都应有明确职责。若一条命令需要横向滚动三屏,最好拆成脚本、加中间变量和错误处理。
Shell:编排流程,而非硬算文本
Shell 最重要的能力是调用命令、展开变量、判断返回码:
1 |
|
这里有几个生产脚本习惯:
"$log_file"必须加引号,避免空格和通配符展开;${1:-app.log}表示参数为空时使用默认值;set -u让未定义变量尽早报错;pipefail让管道中间命令失败时,整条管道也失败;- 错误信息写到
>&2。
循环应当处理文件名,而不是解析 ls 输出:
1 | find logs -type f -name '*.log' -print0 | |
-print0 与 read -d '' 用 NUL 分隔,因此文件名含空格、换行也不怕。
sed:逐行做编辑
sed 的基本形式是:
1 | sed [选项] '地址 + 命令' 文件 |
最常见的是替换:
1 | sed 's/http:/https:/' config.txt # 每行只替换第一次 |
地址用来限定处理范围:
1 | sed -n '10,20p' app.log # 只打印第 10~20 行 |
默认情况下,sed 只把结果打印到 stdout,不修改原文件。这一点很安全:
1 | sed 's/foo/bar/g' file >file.new |
确认结果后再替换原文件。sed -i 会原地修改,但 GNU sed 与 macOS/BSD sed 的参数略有差异:
1 | sed -i.bak 's/foo/bar/g' file # 同时保留 file.bak,兼容性较好 |
sed 适合局部、线性的变换。需要跨多行状态、复杂分组或数值统计时,awk 通常更清晰。
awk:把文本当成表
awk 默认按空白切列:
1 | awk '{print $1, $NF}' access.log |
$1 是第一列,$NF 是最后一列,$0 是整行,NF 是字段数,NR 是累计行号。指定分隔符后,它很像一台轻量表格引擎:
1 | awk -F, 'NR > 1 {sum += $3} END {print "总计:", sum}' sales.csv |
awk 程序通常由三段组成:
1 | BEGIN { 初始化 } |
例如统计 nginx 访问日志里的状态码:
1 | awk '{count[$9]++} |
又比如计算耗时字段的平均值与最大值:
1 | awk ' |
条件模式也很直观:
1 | awk '$9 >= 500 {print $1, $7, $9}' access.log |
注意:真正的 CSV 支持引号内逗号和换行,简单 -F, 无法正确解析所有 CSV。严肃数据处理应使用 Python csv、Miller 或专门工具。
经典单行命令
统计出现频率
1 | awk '{print $1}' access.log | sort | uniq -c | sort -nr | head |
为什么必须先 sort?因为 uniq 只合并相邻重复行。
替换配置但跳过注释
1 | sed '/^[[:space:]]*#/! s/old.example.com/new.example.com/g' app.conf |
/模式/! 表示“不匹配该模式的行”,因此注释保持原样。
找出 P99 之外的慢请求
若日志末列是毫秒耗时,可先做快速分位数观察:
1 | awk '{print $NF}' access.log | sort -n | |
这是离线近似写法;海量数据不宜全部排序,应使用监控系统的直方图或流式分位算法。
同时读取 Shell 变量
1 | threshold=1000 |
不要把 Shell 变量生硬拼进 awk 源码,-v 更安全、可读。
什么时候用哪个
先按问题形状选工具:
- 需要启动多个程序、判断成功失败:Shell。
- 每行做一次替换或删选:sed。
- 按列判断、累加、分组:awk。
- JSON:
jq;YAML:yq。 - 复杂数据结构、网络请求、长期维护:Python/Go 等通用语言。
一个实用边界是:命令用于一次性探索很合适;一旦要进定时任务、会修改数据、需要测试和重试,就把它升级成有参数校验、日志和测试的脚本。
常见陷阱
引号层级
单引号阻止 Shell 展开,双引号允许 $variable、$(command) 展开:
1 | name='world' |
awk 程序通常放单引号里,Shell 值通过 -v 传入。
正则不是通用解析器
HTML、JSON、CSV 都有结构与转义规则。sed/awk 可以处理受控的简单格式,但不应冒充完整解析器。
管道吞掉失败
未启用 pipefail 时,producer | consumer 的退出码通常只看最后一个命令。生产脚本建议:
1 | set -o pipefail |
不要边看边破坏
先输出预览,再写文件;先备份,再用 -i;处理批量文件时用 NUL 分隔;把破坏性动作设计成可重复、可回滚。
小结
Shell、sed、awk 的共同美感是组合,而不是炫技。Shell 管流程,sed 改行,awk 算列;选择合适的数据工具,保持每一级管道简单,再用 set -euo pipefail、正确引用和预览机制兜住边界。
真正高质量的一行命令,不是字符最少,而是读者能快速回答:输入是什么、做了哪些变换、失败会怎样。