Shell 文本处理
更新时间:2026-08-28。本文是 Shell 编程主题第③族:文本处理与日志分析,共 17 个命令,是日志分析、数据清洗、配置处理的绝对主力。
与 Linux 命令速查第②族 的分工:那边是字典式速查(36 个命令,想不起参数时查);本文把其中最常用的 17 个讲透——尤其是
grep/sed/awk这"三剑客"的完整能力,以及怎么把它们串成流水线。
一、搜索与查看
1.1 grep — 文本搜索
grep 用正则表达式匹配文本,是排查问题的第一反应。它的选项可以按用途分几类:控制匹配(-i 忽略大小写、-w 整词匹配、-x 整行匹配、-E 扩展正则、-F 固定字符串不做正则解析因而更快、-P Perl 正则支持 \d 这类语法);控制输出(-n 显示行号、-o 只输出匹配部分而非整行、-c 只计数、-l/-L 只列出有/无匹配的文件名、-A/-B/-C 显示上下文行、--color 高亮、-q 静默不输出);控制搜索范围(-r 递归目录、--include/--exclude 按文件名过滤、-a 把二进制当文本处理)。
有个非常实用的特性:grep 的退出码——匹配到返回 0,没匹配到返回 1,出错返回 2。这让它在脚本里可以直接当条件用:if grep -q "pattern" file; then ...。-m N 匹配到 N 条就停止,在只需要判断"是否存在"时能省下大量时间(尤其对大文件)。
正则要点:. 匹配任意单字符,* 表示前一项重复零次或多次,^/$ 锚定行首行尾,[abc] 字符类,\{n,m\} 重复次数。注意基础正则(BRE)与扩展正则(ERE)的转义差异——用 -E 时 +、?、|、() 都不用转义,写起来清爽得多。
grep "error" app.log # 搜索包含 error 的行
grep -i "error" app.log # 忽略大小写
grep -n "error" app.log # 显示行号
grep -v "debug" app.log # 排除(取反)
grep -r "func(" ./src/ # 递归搜索目录
grep -c "error" app.log # 只计数
grep -o "[0-9]\{3\}" file # 只输出匹配到的部分
grep -w "word" file # 整词匹配
grep -A2 -B2 "error" app.log # 上下文前后各 2 行
grep -E "err|warn" app.log # 扩展正则
grep -m1 "error" big.log # 找到第一条就停
grep -q "key" file && echo 存在 # 静默模式,用于条件判断
grep --include="*.py" -r "import" # 只搜 .py 文件1.2 head 与 tail
head/tail 取文件的头尾部分。-n 指定行数(可简写成 -5),-c 指定字节数——处理超大文件时,用 -c 只看开头几百字节能瞬间完成。head -n -5 是个反直觉但很有用的写法:去掉最后 5 行(负数表示"到倒数第 N 行为止"),处理带汇总行的报表时很方便。tail -n +2 表示从第 2 行开始输出,常用来去掉 CSV 的表头。多文件操作时默认会打印文件名标题,-q 可以关掉。
跟踪日志是 tail 最核心的场景。tail -f 按文件描述符持续跟踪,但日志文件被 logrotate 切割(重命名)后,它就断了——因为 fd 指向的还是旧文件。tail -F 按文件名跟踪,文件被替换后会自动重新打开,长期挂机看日志一律用 -F。--pid=PID 可以在指定进程结束时自动停止跟踪,写监控脚本时用得上。
head -5 file.txt # 前 5 行
head -c 200 file # 前 200 字节
head -n -5 file # 去掉最后 5 行
tail -5 file.txt # 后 5 行
tail -n +2 data.csv # 从第 2 行开始(去表头)
tail -f app.log # 实时跟踪(按 fd)
tail -F app.log # 实时跟踪(按文件名,日志轮转后自动重开)
tail -f --pid=1234 log # PID 结束时停止跟踪1.3 less 与 cat
cat 适合看短文件;看大文件一定要用 less——它是按需读取的,打开几个 GB 的文件也是秒开,而 cat 会把内容全灌进终端,几百 MB 的日志足以让你等上几分钟。cat 的常用选项:-n 显示行号,-b 只给非空行编号,-s 压缩连续空行,-A 显示所有不可见字符(排查 Windows 换行符 ^M 时一目了然)。不带参数时 cat 从标准输入读,可以配合 here document 写文件。
less 进入后的操作是另一套:空格翻页、b 回翻、/pattern 向下搜索、?pattern 向上搜索、n/N 跳到下一个/上一个匹配、G 跳到末尾、g 跳到开头、q 退出。&pattern 是很多人不知道的功能——只显示匹配的行(相当于在 less 里做了次 grep)。-N 显示行号,-S 不折行(长行用左右箭头看),-i 搜索时忽略大小写。less +F file 进入跟踪模式,等同于 tail -f,按 Ctrl-C 可以退回到正常浏览模式,比 tail 更灵活。
cat -n file.txt # 显示行号
cat -A file.txt # 显示不可见字符(找 ^M)
cat -s file.txt # 压缩连续空行
less -N app.log # 带行号分页
less -S app.log # 长行不折行
less +F app.log # 跟踪模式(等同 tail -f)
# less 内:/ 搜索、n 下一个、&pattern 只看匹配行、G 到末尾、q 退出二、流编辑:sed 与 awk
2.1 sed — 流编辑器
sed 逐行处理文本,核心是地址 + 命令的结构。地址可以是行号(5)、范围(1,10)、正则(/pattern/)、或者两者的组合(/start/,/end/);命令最常用的是 s(替换)、p(打印)、d(删除)、i/a/c(插入/追加/替换整行)。
s/old/new/ 默认只替换每行第一个匹配,加 g 才替换全部,加数字 2 表示只替换第 2 个。替换目标里 & 代表整个匹配内容,\1 引用正则分组(需先加 -E 或在 BRE 里写 \(...\))。分隔符不一定是 /——处理路径时用 s|/a/b|/c/d| 或 s#old#new# 能省掉一堆转义。
sed -n 抑制默认输出,配合 p 命令就是"只打印想要的行":sed -n '10,20p' 取 10~20 行,sed -n '/start/,/end/p' 取两个模式之间的内容——这是从日志里截时间段的常用写法。-i 原地修改文件(直接改原文件,没有撤销),建议写成 sed -i.bak 留备份。-e 可以串多个表达式,-f 从文件读脚本(复杂逻辑用它更好维护)。q 命令提前退出,处理大文件时能提速。
sed -n '5p' file # 只打印第 5 行
sed -n '10,20p' file # 打印 10~20 行
sed -n '/start/,/end/p' log # 打印两个模式之间的内容
sed 's/old/new/' file # 替换每行第一个
sed 's/old/new/g' file # 替换全部
sed 's/old/new/2' file # 只替换第 2 个
sed 's|/usr/local|/opt|g' f # 换分隔符,避免转义路径斜杠
sed '/pattern/d' file # 删除匹配行
sed '/^#/d; /^$/d' conf # 删注释行和空行(分号分隔多条)
sed 's/^/## /' file # 行首加前缀
sed '1i\HEADER' file # 首行前插入
sed -i.bak 's/a/b/g' file # 原地修改并留备份
sed -E 's/([0-9]{4})-.*/\1/' # 分组引用 \1
sed '5q' bigfile # 处理到第 5 行就退出(大文件提速)
sed -i各平台行为不一致:GNU/Linux 上-i可以不带参数,macOS/BSD 上必须写成-i ''。写跨平台脚本时要注意,或者干脆用临时文件中转(sed '...' f > tmp && mv tmp f)避免踩坑。
2.2 awk — 字段处理与报表
awk 是"文本的 Excel":它把每行按分隔符切成字段,然后你可以像写程序一样处理。理解这几个内置变量就入门了大半:$0 是整行,$1~$n 是各字段,NF 是当前行的字段数(所以 $NF 就是最后一列),NR 是全局行号,FNR 是当前文件的行号(处理多文件时用来判断是不是第一个文件的表头)。
awk 的程序结构是 模式 { 动作 }:模式省略则对每行都执行,动作省略则默认打印整行。BEGIN{} 在处理任何输入前执行一次(常用来设分隔符、打印表头),END{} 在全部处理完后执行一次(用来输出汇总结果)。-F 指定输入分隔符,输出的字段分隔符用 OFS 变量控制(在 BEGIN 里设)。-v var=value 把 Shell 变量传进 awk。
它支持数组(包括用字符串做下标的关联数组),这让"分组统计"变得极其简洁。{a[$1]++} END {for (k in a) print k, a[k]} 一行就完成了"按第一列分组计数"——这在纯 Shell 里要写好几行循环。内置函数也够日常用:length()、substr()、split()、index()、toupper()/tolower()、gsub()/sub()(正则替换)、sprintf()(格式化)。
awk '{print $1}' file # 第 1 列
awk '{print $NF}' file # 最后一列
awk -F: '{print $1}' /etc/passwd # 指定分隔符
awk 'NF>3' file # 字段数大于 3 的行
awk 'NR>1 {sum+=$3} END{print sum}' f # 跳过表头求和
awk 'BEGIN{OFS=","} {print $1,$2}' f # 输出用逗号分隔
awk '{a[$1]++} END{for(k in a) print k, a[k]}' f # 分组计数
awk 'FNR==1 && NR!=1 {next} {print}' *.csv # 合并多个 CSV 只留一个表头
awk -v n=100 '$3>n {print}' f # 传入 Shell 变量
awk '{printf "%-20s %8.2f\n", $1, $2}' f # 格式化输出
awk 'length($0)>80' file # 超长行
FNR==NR是处理两个文件的惯用法:FNR是当前文件行号,NR是全局行号,只有读第一个文件时两者才相等。所以awk 'FNR==NR {set[$1]; next} !($1 in set)' whitelist.txt access.log能找出"不在白名单里的记录"——这种关联查询用别的工具写会很啰嗦。awk 的正则匹配用~和!~(如$1 ~ /^ERROR/)。
三、排序、去重与统计
3.1 sort
sort 默认按字典序排,但对数字排序必须用 -n——否则 "10" 会排在 "9" 前面(按字符串比较)。-r 倒序,-u 排序并去重(等价于 sort | uniq,但更快)。-k 指定按第几列排(-k2),配合 -t 指定列分隔符(sort -t: -k3 -n 就是按冒号分隔的第 3 列数值排序)。-k2,2 表示"只按第 2 列排",而 -k2 是"从第 2 列到行尾",处理多列时这个区别很重要。
进阶选项:-h 按人类可读的数值排序(能正确排 1K、2M、3G),看 du -h 的输出时必用;-V 按版本号排序(能正确排 v1.10 和 v1.9);-f 忽略大小写;-o 输出到文件(可以安全地写回原文件,而 sort f > f 会清空文件);-m 合并已排序的文件(比重新排序快);-s 稳定排序(保持相同键值的原有顺序);-R 随机排序。
sort file # 字典序
sort -n file # 数值排序
sort -r file # 倒序
sort -u file # 排序并去重
sort -t: -k3 -n /etc/passwd # 按冒号分隔的第 3 列数值排序
sort -k2,2 file # 只按第 2 列排
sort -h sizes.txt # 按 1K/2M/3G 正确排序
sort -V versions.txt # 版本号排序
sort -o file file # 安全写回原文件
sort -m a b # 合并已排序文件(快)3.2 uniq
uniq 只能合并相邻的重复行——这是它最大的限制,所以几乎总是跟在 sort 后面用:sort | uniq -c 是统计出现次数的标准组合。-c 在每行前加上出现次数,-d 只显示重复出现过的行,-u 只显示唯一的行,-i 忽略大小写。-f N 跳过前 N 个字段再比较,-s N 跳过前 N 个字符——处理带行号或时间戳的日志时有用。
最经典的组合是 sort | uniq -c | sort -rn | head:先排序让重复行相邻,再计数,然后按次数倒序,最后取前几名。日志里找出访问最多的 IP、出现最多的错误类型,一条命令就够。
uniq file # 去重(只去相邻重复)
sort file | uniq -c # 统计出现次数
uniq -d file # 只显示重复行
uniq -u file # 只显示唯一行
uniq -i file # 忽略大小写
sort log | uniq -c | sort -rn | head -10 # TOP 103.3 wc
wc 统计行数、词数、字节数。三个选项要分清:-l 行数、-w 词数(按空白分隔)、-c 字节数。还有 -m 统计字符数——对中文等多字节文本,-m 和 -c 结果不同(UTF-8 下一个中文 3 字节但算 1 个字符)。-L 输出最长行的长度,排查"哪行特别长"时用。多文件时它会输出合计行,配合 tail -1 可以取总数。
注意一个易错点:如果文件最后一行没有换行符,wc -l 的结果会比实际行数少 1,因为它是数换行符的个数。这在处理某些程序生成的文件时会造成困惑。
wc -l file # 行数
wc -w file # 词数
wc -c file # 字节数
wc -m file # 字符数(中文与 -c 不同)
wc -L file # 最长行的长度
wc -l *.log # 多文件,带合计四、列处理与拼接
4.1 cut
cut 按分隔符或字符位置切列。-d 指定分隔符(只能是单个字符),-f 选字段(支持 1,3、2-4、3- 这种写法)。-c 按字符位置切(-c1-10 取前 10 个字符),-b 按字节切。--complement 取反(选中未指定的列),--output-delimiter 指定输出的分隔符。
它的主要局限:无法处理连续多个分隔符。cut -d' ' -f2 遇到 "a b"(多个空格)会取到空字符串,因为它把每个空格都当成一个分隔符。这种情况要改用 awk '{print $2}'(awk 默认把连续空白当成一个分隔符),这也是为什么很多人处理日志时更偏好 awk。
cut -d: -f1 /etc/passwd # 冒号分隔取第 1 列
cut -d, -f1,3 data.csv # 取第 1、3 列
cut -d, -f2- data.csv # 从第 2 列到末尾
cut -c1-10 file # 取前 10 个字符
cut -d: -f1 --complement f # 取反(除第 1 列外都要)
cut -d: -f1 --output-delimiter=, f # 输出用逗号分隔4.2 paste 与 join
paste 把多个文件按行并排拼接(横向合并),-d 指定连接符,-s 把单个文件的所有行转成一行(行列转置)。它不做任何匹配,就是简单地"第 1 行接第 1 行",所以要求各文件行数对应。
join 则是按共同字段连接两个文件,类似 SQL 的 JOIN。前提是两个文件都已按该字段排序,否则结果会漏。-t 指定分隔符,-1/-2 分别指定两个文件用第几列做连接键,-a 1 表示即使右表没有匹配也输出左表的行(类似 LEFT JOIN),-e 给缺失字段填默认值,-o 控制输出哪些列。日常用得不算多,但处理结构化文本时比写脚本省事。
paste a.txt b.txt # 按行并排
paste -d, a.txt b.txt # 用逗号连接
paste -s numbers.txt # 把一列转成一行
join -t, -1 1 -2 1 a.csv b.csv # 按第 1 列连接(需先排序)
join -a 1 -e "NULL" a b # 保留左表未匹配行,缺失填 NULL4.3 tr
tr 做字符级的替换与删除,它逐字符处理,不支持字符串替换(那是 sed 的活)。-d 删除指定字符,-s 压缩连续重复字符,-c 取补集(对"除了这些字符之外"操作)。支持 POSIX 字符类([:upper:]、[:lower:]、[:digit:]、[:space:])和范围(a-z)。
最经典的三个用法:tr 'a-z' 'A-Z' 转大写、tr -d '\r' 删除 Windows 换行符(或用 dos2unix)、tr -s ' ' 把连续空格压成一个(配合 cut 处理不规则对齐的文本时特别有用)。-c 的妙用:tr -c '[:print:]' '\n' < binary 能把不可打印字符变成换行,从二进制里提取可读片段。
echo abc | tr 'a-z' 'A-Z' # 转大写
tr -d '\r' < win.txt > unix.txt # 去 CR(Windows→Unix)
tr -s ' ' < file # 压缩连续空格
tr -cd '[:digit:]\n' < file # 只保留数字和换行(-c 补集 + -d 删除)
tr '[:upper:]' '[:lower:]' < f # 用字符类转小写4.4 tee
tee 让数据分流:一边输出到屏幕,一边写进文件,是管道的"三通接头"。-a 追加而非覆盖。它可以同时写多个文件(tee a.log b.log)。
最巧妙的用法是配合 sudo:sudo echo x > /etc/file 会失败——因为重定向是由 Shell(普通用户权限)执行的,sudo 只作用到 echo。但 echo x | sudo tee /etc/file 就能成功,因为写文件的动作由 tee 以 root 身份完成。调试长管道时,在中间插 tee /tmp/step.txt 可以把那一步的数据存下来看,而流程继续往下走。
cmd | tee out.log # 输出到屏幕同时写文件
cmd | tee -a out.log # 追加
cmd 2>&1 | tee run.log # 错误输出也一起记录
echo x | sudo tee /etc/file # 写入需要 root 的文件
cmd1 | tee /tmp/s1 | cmd2 # 管道中间取样调试4.5 diff 与 comm
diff 比较两个文件的差异。-u 输出 unified 格式(带上下文,是生成补丁的标准格式,diff -u old new > fix.patch 之后用 patch 应用),-c 是 context 格式,-y 并排显示(配合 -W 120 设宽度,肉眼对比很直观),-r 递归比较目录,-i 忽略大小写,-b/-w 忽略空白差异,-q 只报"是否相同"不列细节(比较目录时很有用)。diff 的退出码:相同为 0,不同为 1,出错为 2,脚本里可以直接判断。
comm 比较两个已排序文件的行,输出三列:只在第一个文件、只在第二个文件、两者共有。用 -1、-2、-3 抑制对应列,于是 comm -12 a b 得到交集,comm -23 a b 得到"只在 a 中"(差集)。做"找出新增/移除的项"(比如对比两次的包列表)时,它比 diff 的输出更好解析。
diff a b # 基本比较
diff -u a b > fix.patch # 生成补丁
diff -y -W 120 a b # 并排对比
diff -r dir1 dir2 # 递归比较目录
diff -q a b && echo 相同 # 只判断是否相同
diff -i -b a b # 忽略大小写和空白
comm -12 a b # 交集(两文件共有)
comm -23 a b # 只在 a 中(差集)五、三剑客流水线实战
单个命令都很简单,难的是怎么把它们按顺序串起来。下面是一条真实的日志分析流水线——从原始访问日志到统计报表,每一步只做一个动作。
5.1 从原始日志到统计报表
# 统计各状态码出现次数(按次数倒序)
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 找出耗时最长的前 10 个请求(假设最后一列是响应时间)
awk '{print $NF, $7}' access.log | sort -rn | head -10
# 统计每个 IP 的请求数,只显示超过 100 次的
awk '{a[$1]++} END{for(ip in a) if(a[ip]>100) print a[ip], ip}' access.log | sort -rn
# 按小时统计请求量
awk -F'[ :]' '{print $4}' access.log | sort | uniq -c这条链的逻辑是固定的:提取字段(awk/cut)→ 排序(sort)→ 计数(uniq -c)→ 按次数倒序(sort -rn)。uniq 只能合并相邻的重复行,所以 sort 必须在它前面——这是最常被忘记的一步。
5.2 awk 做分组统计
# 按某列分组求和(跳过 CSV 表头)
awk -F, 'NR>1 {sum[$2] += $3} END {for (k in sum) printf "%-20s %10.2f\n", k, sum[k]}' data.csv
# 计算错误率
awk '{total++; if ($9 >= 500) err++} END {printf "错误率: %.2f%% (%d/%d)\n", err/total*100, err, total}' access.log
# 两个文件关联查询(FNR==NR 技巧)
awk 'FNR==NR {allow[$1]=1; next} !($1 in allow) {print "未授权:", $1}' whitelist.txt access.logawk 的 END{} 块在所有输入读完后执行一次,是做汇总统计的天然位置。上面的错误率统计一行就能替代一段脚本,这也是它在日志分析里无可替代的原因。
5.3 sed 批量替换的注意事项
# 先演练:只看会改什么,不实际写入
sed -n 's/old/new/gp' config.ini
# 确认后原地修改并留备份
sed -i.bak 's/old/new/g' config.ini
# 只在匹配的行里替换(地址限定)
sed -i '/^\[database\]/,/^\[/ s/host=.*/host=newhost/' config.ini
# 删除空行和注释行
sed -E '/^\s*(#|$)/d' config.inised -i 会直接改文件,没有撤销。养成"先 sed -n ...p 演练、再 -i.bak 落地"的习惯——.bak 后缀会生成备份文件,出问题时还能救回来。
5.4 管道调试技巧
# 长管道中间插 tee,看每一步的中间结果
grep ERROR access.log | tee /tmp/s1 | awk '{print $5}' | tee /tmp/s2 | sort | uniq -c
# 让管道中任一环节失败都被捕获
set -o pipefail
grep pattern file | wc -l || echo "管道中出错了"管道长了以后,一旦结果不对很难定位在哪一步。临时插几个 tee 把中间结果存下来是最快的排查方法。另外默认 Shell 只检查管道最后一个命令的退出码,前面的环节失败了也不会报错——set -o pipefail 就是为解决这个而存在的。
六、与主线衔接
- 文本处理与文件查找配合,见 文件与目录命令。
- 处理进程输出、
ps结果时,本文的字段提取技巧直接适用,见 进程与系统命令。 - 更完整的命令参数速查(含
csplit、fmt、dos2unix、nl等),见 Linux 命令速查第②族。 - JSON/YAML/CSV 这类结构化数据,专用工具更好用,见 结构化数据处理。
一句话总结
文本处理 = 搜(grep)+ 改(sed)+ 算(awk)+ 排(sort | uniq -c)+ 切(cut)+ 比(diff/comm);串成流水线时记住四条——uniq 前必须先 sort,cut 处理不了连续空白改用 awk,sed -i 前先 sed -n 演练,长管道用 tee 分段排查并配 set -o pipefail。