Linux 基础体系 · 第 42/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux 文本处理:grep、sed、awk、cut、sort、uniq 和正则
在 Linux 中,“文本处理”通常不是某个命令独立完成全部工作,而是由多个只负责一小部分变换的程序组成管道:
文件或标准输入
│
▼
grep:筛选记录
│
▼
sed:按规则替换或删除
│
▼
awk:按字段计算和格式化
│
▼
cut:提取固定字段
│
▼
sort:排序
│
▼
uniq:合并相邻重复记录
│
▼
标准输出、文件或下一个程序
这里的“记录”通常是一行,但具体含义由程序决定:
grep默认以行为匹配单位;sed默认逐行读取,内部称为 pattern space;awk默认以行为记录,以空白分隔字段;cut、sort和uniq也通常按行工作,但对字段、排序键和重复关系的定义不同。
这些命令主要处理结构简单、边界明确的文本流。它们不是通用的 CSV、JSON 或 YAML 解析器。遇到嵌套结构、引号中的分隔符或转义规则时,应使用对应的结构化工具,例如 jq 或 yq,而不是继续叠加正则和 cut。
一、先理解 Shell 管道:程序处理的是字节流,不是“变量列表”
命令:
grep -E 'ERROR|WARN' app.log | awk '{print $1, $2}'
可以分成三步:
grep从app.log读取内容;grep将匹配的行写入标准输出;- Shell 将前一个程序的标准输出连接到
awk的标准输入; awk处理收到的每一行。
管道传递的是字节流。Shell 不会自动理解“这一行是一个对象”或“这个字段是一个整数”。字段如何划分、数字如何比较,必须由命令自己定义。
1. 标准输入、标准输出和重定向
grep -i 'timeout' app.log
grep -i 'timeout' < app.log
grep -i 'timeout' app.log > timeout.log
三者的区别是:
- 第一条把
app.log作为grep的文件参数; - 第二条通过标准输入提供内容;
- 第三条将标准输出写入
timeout.log。
重定向文件时,Shell 通常会先创建或截断目标文件,再启动命令。因此下面的写法会在命令启动前清空 app.log:
grep 'ERROR' app.log > app.log
安全的做法是使用临时文件并在验证后替换:
tmp=$(mktemp)
if grep 'ERROR' app.log >"$tmp"; then
mv -- "$tmp" error.log
else
rm -f -- "$tmp"
fi
这里的 grep 退出状态也很重要:
0:至少有一行匹配;1:没有匹配;2:发生错误,例如文件不存在或正则无效。
因此,“没有匹配”不一定是命令失败,但在 Shell 的 set -e、CI 或监控脚本中必须明确区分这几种状态。
2. 管道失败状态
默认情况下,Shell 管道的退出状态通常是最后一个命令的状态:
grep 'ERROR' missing.log | sort
echo "$?"
即使 grep 因文件不存在失败,sort 仍可能成功,导致整个管道返回成功。Bash、Zsh 等支持:
set -o pipefail
启用后,管道在任一命令失败时通常能够反映失败,但“无匹配”的 grep 状态 1 是否算业务失败,仍需要脚本自行处理。
二、正则表达式:描述字符串集合的规则
1. 正则的基本概念
正则表达式可以看作一个描述字符串集合的表达式。设正则表达式为 ,其匹配语言记作 。例如:
cat|dog
表示字符串集合:
{"cat", "dog"}
而:
[0-9]+
表示一个或多个 ASCII 数字组成的字符串集合,例如:
1
42
20250101
在命令行中,正则通常被用来判断“某一行是否包含满足条件的子串”。这与“整行必须完全满足条件”不同:
printf '%s\n' 'id=42' 'abc' | grep -E '[0-9]+'
输出:
id=42
因为 [0-9]+ 只需在行中找到一个匹配片段。若要求整行只能由数字组成,需要使用锚点:
printf '%s\n' '42' 'id=42' '42x' | grep -E '^[0-9]+$'
输出:
42
其中:
^匹配行首;$匹配行尾;[0-9]匹配一个数字;+表示前一个原子出现一次或多次。
可以将这个条件形式化为:
^ 和 $ 将“字符串中存在匹配”收紧为“整个字符串属于该语言”。
2. 常见正则运算符
| 写法 | 含义 |
|---|---|
. |
任意单个字符,通常不匹配换行 |
[abc] |
a、b、c 中任意一个 |
[^abc] |
不属于 a、b、c 的一个字符 |
[[:digit:]] |
当前 locale 定义的数字字符类 |
* |
前一个原子出现零次或多次 |
+ |
前一个原子出现一次或多次 |
? |
前一个原子出现零次或一次 |
{m,n} |
出现至少 m 次、至多 n 次 |
(...) |
分组 |
| ` | ` |
^、$ |
行首、行尾 |
+、?、|、括号和 {m,n} 在扩展正则表达式中更自然,因此通常使用 grep -E、sed -E 或 awk 的正则语法。
grep -E '^(ERROR|WARN):' app.log
这表示整行开头必须是 ERROR: 或 WARN:。
3. BRE、ERE 和 PCRE 的区别
grep 默认使用基本正则表达式(BRE):
grep 'ab\+' file
在 GNU grep 中,\+ 可表示一次或多次,但这种写法不适合依赖跨实现兼容性。
扩展正则表达式(ERE)使用:
grep -E 'ab+' file
sed 默认也使用 BRE,使用 -E 可启用 ERE:
sed -E 's/[0-9]+/<number>/g' file
grep -P 使用 Perl 兼容正则表达式,但它不是 POSIX 标准接口,在不同发行版、不同 grep 构建方式中支持情况可能不同。除非明确依赖 PCRE 特性,否则优先使用 grep -E。
4. Shell 引号和正则不是一回事
Shell 会先解释命令行,再把参数传给程序。正则最好放在单引号中:
grep -E '^[0-9]+$' file
单引号可以防止 Shell 展开 $、反引号、命令替换以及通配符。下面的双引号虽然也能保护大多数正则字符,但 $ 可能被 Shell 当作变量展开:
grep -E "^[0-9]+$" file
更稳妥的是:
grep -E '^[0-9]+$' file
正则中的 * 和 Shell 文件名通配符中的 * 也不是同一个机制:
grep '*.log' file
这里的 * 是正则量词,表示前一个字符 ' 重复零次或多次,通常不是“匹配任意字符”。如果要匹配字面量点号:
grep -E '\.log$' file
三、grep:筛选包含匹配内容的行
grep 的核心语义是:
对输入中的每一行进行匹配,输出满足条件的行。
1. 基本用法
先准备示例文件:
cat >app.log <<'EOF'
2025-03-01 INFO user=alice action=login
2025-03-01 ERROR user=bob action=delete
2025-03-01 WARN user=alice action=retry
2025-03-02 ERROR user=carol action=login
EOF
筛选包含 ERROR 的行:
grep 'ERROR' app.log
输出:
2025-03-01 ERROR user=bob action=delete
2025-03-02 ERROR user=carol action=login
常用选项:
grep -i 'error' app.log # 忽略大小写
grep -v 'ERROR' app.log # 输出不匹配的行
grep -n 'ERROR' app.log # 输出行号
grep -c 'ERROR' app.log # 统计匹配行数
grep -l 'ERROR' *.log # 只输出包含匹配的文件名
grep -h 'ERROR' *.log # 多文件输出时隐藏文件名
grep -F 'a.b' file # 固定字符串匹配,不解释正则
grep -E 'ERROR|WARN' app.log # 扩展正则
grep -F 对搜索用户输入的字面量尤其重要。如果用户输入:
a.b
普通正则会将 . 解释为任意字符,可能额外匹配 aXb。固定字符串模式则只匹配字面量 a.b。
2. 单词边界和文件名边界
grep -w 'run' file
-w 常用于匹配单词,但“单词字符”的定义与实现和 locale 有关,不能简单等同于编程语言中的标识符规则。
处理可能以 - 开头的文件名时,应使用 --:
grep -- '-n' -- weird-file
这里第一个 -- 结束选项,后面的 -n 被当作模式参数。更常见的写法是明确使用 -e:
grep -e '-n' -- weird-file
3. grep 的边界
grep 输出的是原始行。它不会:
- 解析 CSV 的引号和转义;
- 理解 JSON 对象层次;
- 将日期自动转换为日期值;
- 根据数字大小进行排序;
- 将重复的非相邻行自动合并。
例如:
printf '%s\n' 'name="a,b",age=20' | cut -d, -f2
结果会被错误地按逗号切成多个部分,因为 CSV 中的逗号可能位于引号内。此时应使用 CSV 解析器,而不是 cut。
四、sed:逐行执行编辑脚本
sed 是流编辑器。它通常不会把整个文件装入内存,而是循环执行:
- 读取一行到 pattern space;
- 执行编辑命令;
- 默认输出 pattern space;
- 读取下一行。
可以抽象为:
输入行 → pattern space → sed 脚本 → 输出或丢弃
1. 替换命令 s
最常见的形式:
s/正则/替换文本/标志
例如:
printf '%s\n' 'user=alice' 'user=bob' |
sed -E 's/user=([a-z]+)/account=\1/'
输出:
account=alice
account=bob
([a-z]+) 是捕获组,\1 引用第一个捕获组。
默认情况下,s 只替换每行第一个匹配:
printf '%s\n' 'a a a' | sed 's/a/x/'
输出:
x a a
加上 g 才替换整行所有匹配:
printf '%s\n' 'a a a' | sed 's/a/x/g'
输出:
x x x
其他常见标志:
sed -n 's/ERROR/FAIL/p' app.log
-n 关闭默认输出,p 只打印发生替换的行。因此这条命令只输出实际包含并成功替换 ERROR 的行。
2. 地址:只对部分行执行命令
地址决定命令作用在哪些行:
sed -n '2p' app.log
sed -n '/ERROR/p' app.log
sed -n '2,4p' app.log
sed -n '1,/WARN/p' app.log
例如:
sed -n '/ERROR/ s/user=/account=/p' app.log
处理顺序是:
- 当前行是否匹配
/ERROR/; - 若匹配,执行替换;
- 只有替换成功的行才由
p输出。
删除注释和空行:
sed -E '/^[[:space:]]*#/d; /^[[:space:]]*$/d' config.txt
这里分号分隔两条命令:
- 匹配注释行则删除;
- 匹配空白行则删除。
3. -i 原地修改的风险
sed -i 's/old/new/g' config.txt
这是 GNU sed 常见用法,但它通常会通过临时文件和重命名完成更新,权限、符号链接行为、硬链接关系和不同 sed 实现的细节可能存在差异。macOS 的 BSD sed 对 -i 参数形式也不同,因此不能把 GNU 命令直接视为所有 Unix 的通用语法。
生产配置修改应先备份或生成新文件:
cp -- config.txt config.txt.bak
sed -E 's/^timeout=.*/timeout=30/' config.txt >config.txt.new
diff -u config.txt config.txt.new
mv -- config.txt.new config.txt
如果程序需要原子更新,临时文件应与目标位于同一文件系统,并在校验成功后使用 mv 替换。若配置更新过程涉及权限、属主、SELinux 标签或服务重载,还必须额外验证这些属性。
4. sed 不是通用多行解析器
sed 可以使用 N、D、P 等命令处理多行 pattern space,但脚本复杂度会迅速增加。它适合规则明确的流式替换、删除、抽取;对于嵌套 JSON、YAML 或需要语法验证的配置,不应依赖 sed 做结构化修改。
五、awk:按记录和字段进行计算
awk 不只是“按空格切字段”。它是一个按记录处理的文本编程语言,基本结构是:
pattern { action }
对每条记录:
- 计算
pattern; - 如果条件为真,执行
action; - 继续处理下一条记录。
1. 记录、字段和内置变量
默认情况下:
RS:记录分隔符,通常是换行;FS:字段分隔符,默认是空白;$0:整条记录;$1、$2:第一个、第二个字段;NF:当前记录字段数;NR:当前输入的总记录号;FNR:当前文件内的记录号;OFS:输出字段分隔符,默认是空格;ORS:输出记录分隔符,默认是换行。
示例:
awk '{print NR, $2, $NF}' app.log
输出类似:
1 INFO login
2 ERROR delete
3 WARN retry
4 ERROR login
这里 $NF 表示最后一个字段,因此不需要知道每行字段总数。
2. 条件过滤和数值计算
awk '$2 == "ERROR" { count++ } END { print count+0 }' app.log
处理逻辑是:
- 每读一行,比较第二字段是否等于
ERROR; - 匹配时将
count加一; - 全部输入结束后执行
END; count+0确保没有匹配时输出0,而不是空值。
按用户统计错误数:
awk '
$2 == "ERROR" {
user = $3
sub(/^user=/, "", user)
errors[user]++
}
END {
for (user in errors)
print user, errors[user]
}
' app.log
可能输出:
bob 1
carol 1
for (user in errors) 的遍历顺序不保证。若需要稳定输出,应再排序:
awk '
$2 == "ERROR" {
user = $3
sub(/^user=/, "", user)
errors[user]++
}
END {
for (user in errors)
print user, errors[user]
}
' app.log | LC_ALL=C sort
3. BEGIN、END 和格式控制
统计不同级别:
awk '
BEGIN { OFS="\t" }
{
level[$2]++
}
END {
for (x in level)
print x, level[x]
}
' app.log
BEGIN 在读取第一条记录前执行,适合初始化分隔符和变量;END 在所有输入处理完成后执行,适合汇总结果。
指定字段分隔符:
awk -F: '{ print $1, $3, $7 }' /etc/passwd
/etc/passwd 的字段由单个冒号分隔,因此 -F: 合适。输出默认使用空格。
对于连续空白,awk 的特殊默认行为很有用:
awk '{ print $1, $2 }' file
默认 FS 是一个特殊的空白分隔规则,会把连续空格和制表符视为分隔符,并忽略行首行尾空白。但如果明确写:
awk -F' ' '{ print $1, $2 }' file
在常见 awk 中仍有相近的特殊处理;若需要严格区分空格字符和制表符,应使用明确正则,例如 -F'\t'。
4. NR 和 FNR 的差异
awk '{ print FILENAME, FNR, NR, $0 }' a.txt b.txt
假设两个文件各有两行,输出中的 FNR 会在读取 b.txt 时重新从 1 开始,而 NR 继续累计。比较两者是处理多个输入文件时识别文件边界的基础。
5. awk 的正则匹配
awk '$2 ~ /^ERROR$/ { print $0 }' app.log
~:字段匹配正则;!~:字段不匹配正则;/^ERROR$/:整字段必须等于ERROR。
这与:
awk '$2 == "ERROR" { print }' app.log
在这个例子中效果相似,但前者可以表达更复杂的模式。
6. awk 中的字符串和数字转换
awk 具有字符串和数字的双重转换语义:
awk 'BEGIN {
print ("10" + 2)
print ("10" < "2")
print (10 < 2)
}'
具体比较结果受操作数类型和 awk 实现规则影响,不应把所有输入都当作可靠数字。处理外部数据时,先验证格式:
awk '$1 ~ /^[0-9]+$/ && $1+0 >= 100 { print }' file
需要注意,awk 的整数精度和浮点行为依赖实现及底层类型。金额、超大整数或严格十进制计算不应直接交给 awk 做最终财务计算。
六、cut:按位置或单字符分隔符提取字段
cut 设计目标是提取固定位置,不是通用分隔格式解析。
1. 按字符、字节和字段提取
cut -c 1-8 file # 按字符位置
cut -b 1-8 file # 按字节位置
cut -d: -f1,3 /etc/passwd
三种模式:
-c:字符位置;-b:字节位置;-f:字段位置;-d:字段分隔符,通常是单个字符;--complement:取未选中的部分。
中文环境下,-b 可能截断一个多字节 UTF-8 字符;-c 按字符处理,但具体 locale 会影响字符定义。需要稳定的字节级协议处理时使用 LC_ALL=C,需要按本地字符处理时则明确设置 UTF-8 locale。
2. 字段提取示例
printf '%s\n' \
'alice:1001:/home/alice' \
'bob:1002:/home/bob' |
cut -d: -f1,3
输出:
alice:/home/alice
bob:/home/bob
cut 的 -d 只表示一个字段分隔字符。它不能直接表达:
- 任意数量的空白;
- 多个候选分隔符;
- 引号中的分隔符;
- 需要转义的字段内容。
因此,以下任务通常更适合 awk:
awk -F'[[:space:]]+' '{ print $1, $3 }' file
3. 缺失分隔符的边界
默认情况下,如果一行不含指定分隔符,GNU cut -d: -f1 通常会把整行视为字段内容,而 -f 的组合行为还受 -s 影响:
cut -d: -f1 file
cut -d: -f1 -s file
-s 表示不输出不含分隔符的行。处理可能损坏或格式不统一的文件时,应明确决定:是保留异常行供诊断,还是静默丢弃它们。直接使用 -s 可能掩盖输入问题。
七、sort:定义全序并重新排列记录
sort 通常对每一行建立排序键,然后按照比较规则输出有序结果。排序顺序受 locale、数值解释、字段键和稳定性选项影响。
1. 基本排序
printf '%s\n' 20 3 100 11 | sort
默认是字典序,输出:
100
11
20
3
因为比较的是字符串:
"100" < "11" < "20" < "3"
若要按数值排序:
printf '%s\n' 20 3 100 11 | sort -n
输出:
3
11
20
100
sort -n 适用于常见十进制整数和固定格式数字。带单位、货币符号、科学计数法或复杂数值时,应先规范化,或使用其他语言进行解析。
2. 字段和键
对于空白分隔的输入:
alice 42
bob 7
carol 19
按第二列数值排序:
sort -k2,2n users.txt
-k2,2n 的含义是:
- 排序键从第 2 字段开始;
- 到第 2 字段结束;
- 使用数值比较。
不要只写:
sort -k2n users.txt
因为键可能从第 2 字段延伸到行尾,后续字段会参与比较,可能产生意料之外的结果。
指定分隔符:
sort -t: -k3,3n /etc/passwd
按冒号分隔,使用第 3 字段的数值排序。
常用选项:
sort -r # 逆序
sort -u # 排序后只保留每个相等键的一行
sort -f # 忽略大小写
sort -h # 人类可读数字,如 10K、2M,GNU 扩展
sort -c # 检查输入是否已有序
sort -C # 检查是否有序但不输出
3. locale 会改变排序结果
sort file
的排序规则可能受到 LC_COLLATE 影响。为了在日志、构建和测试中获得可复现结果,常见做法是:
LC_ALL=C sort file
但这不是“总是更正确”。LC_ALL=C 往往按字节或 C locale 规则排序,不一定符合用户语言环境中的字典序。选择 locale 是接口语义的一部分,而不是单纯性能开关。
4. 大文件和临时空间
sort 可能使用内存和临时文件完成外部排序:
sort -T /var/tmp large.log
如果临时目录空间不足,排序会失败;如果输入包含敏感信息,临时文件位置和权限也需要考虑。生产环境应检查磁盘空间、TMPDIR、-T 配置以及失败后的清理路径。
八、uniq:只合并相邻相同记录
uniq 的核心定义不是“删除所有重复行”,而是:
将相邻且相等的输入行压缩为一行。
反例:
printf '%s\n' a b a | uniq
输出仍是:
a
b
a
因为两个 a 不相邻。
如果要统计所有重复项,通常先排序:
printf '%s\n' a b a a b | sort | uniq -c
输出:
3 a
2 b
这里的中间结果是:
a
a
a
b
b
uniq -c 对每个连续组计数,sort 先把相同值放入同一组,所以计数才覆盖全部输入。
常用选项:
uniq -c # 每组前输出计数
uniq -d # 只输出重复组
uniq -u # 只输出只出现一次的组
uniq -f N # 比较时跳过前 N 个字段
uniq -w N # 只比较前 N 个字符
sort -u 和 sort | uniq 的区别
sort -u file
sort file | uniq
对“整行去重”的常见输入,结果通常相同,但语义和资源特征不完全相同:
sort -u在排序阶段处理相等项;sort | uniq先完成排序,再由 uniq 合并相邻项;- 若需要计数、只看重复项等,必须使用
uniq的相应选项; - 排序比较规则会受到
sort的选项和 locale 影响,而uniq的相等比较也应与排序条件保持一致。
一个常见错误是:
sort -k2,2n file | uniq
如果目标是按第二字段去重,这条命令并没有做到,因为 uniq 默认比较整行。需要先明确“相等”的定义,例如:
sort -k2,2n -k1,1 file
但这仍是按整行去重;如果只想按某个字段保留一条记录,通常使用 awk:
awk '!seen[$2]++' file
这表示对每个第二字段只输出第一次出现的记录。它不排序,输出顺序是首次出现顺序,并且会在内存中保存已见键。
九、组合算例:从日志中统计错误用户
输入为:
2025-03-01 INFO user=alice action=login
2025-03-01 ERROR user=bob action=delete
2025-03-01 WARN user=alice action=retry
2025-03-02 ERROR user=carol action=login
目标:
- 只处理
ERROR行; - 提取用户;
- 统计每个用户的错误数;
- 按错误次数降序输出。
可以直接使用 awk:
awk '
$2 == "ERROR" {
user = $3
sub(/^user=/, "", user)
count[user]++
}
END {
for (user in count)
print count[user], user
}
' app.log | sort -k1,1nr -k2,2
中间数据为:
1 bob
1 carol
最终输出:
1 bob
1 carol
如果 bob 出现三次,则会得到:
3 bob
1 carol
并按数值降序排列。
也可以拆成管道,以便逐步检查:
grep -E '^[^ ]+ ERROR ' app.log |
awk '{
user = $3
sub(/^user=/, "", user)
print user
}' |
sort |
uniq -c |
sort -k1,1nr -k2,2
每一步的输入输出契约是:
grep:输出第二列为ERROR的完整行;awk:从第三字段去掉user=,输出纯用户名;- 第一个
sort:将相同用户名排列到一起; uniq -c:统计每个相邻用户名组;- 第二个
sort:按计数降序,再按用户名排序。
分步管道更容易诊断,但中间输出必须保证不会把控制字符、换行或分隔符混入字段。若用户名来自不可信输入,最好使用明确的编码或结构化输出格式。
十、正则匹配和文本替换中的常见失败
1. 未锚定导致“部分匹配”
grep -E '[0-9]+' input
会接受:
abc123
123xyz
若输入字段必须是纯数字:
grep -E '^[0-9]+$' input
若文件可能使用 CRLF,行尾可能包含回车字符,导致 $ 前实际还有 \r。可以诊断:
cat -vet input
od -An -tx1 -c input
处理文本转换时可使用专门工具或明确的 sed 规则,但不要在不了解文件格式时直接删除所有控制字符。
2. grep 的正则与 Shell 通配符混淆
查找字面量文件扩展名:
grep -E '\.log$' file
而不是:
grep '*.log' file
如果搜索内容来自变量,并且需求是字面量搜索:
needle='a.b'
grep -F -- "$needle" file
这里同时解决了两个问题:
-F禁止正则解释;--防止以-开头的内容被当成选项。
3. sed 替换内容包含分隔符
默认使用 / 作为分隔符:
sed 's/old/path/new/path/g' file
这会产生歧义。可以选择其他分隔符:
sed 's#old/path#new/path#g' file
但替换文本中的 & 具有特殊含义,表示整个匹配内容;若要插入字面量 &,需要转义:
printf '%s\n' 'a' | sed 's/a/A\&B/'
输出:
A&B
4. awk 输出字段时不要误解 $0
awk '{$1=$1; print}' file
这条常用于按 awk 的 OFS 重新格式化空白。赋值 $1=$1 会使 awk 重建 $0,连续空白可能被压缩,行首行尾空白也可能改变。它不是无损处理。
十一、文本编码、换行和 locale
这些命令通常处理字节序列,并通过 locale 判断字符类别、大小写和排序关系。以下命令的结果可能受 locale 影响:
grep -i
grep '[[:alpha:]]'
sort
cut -c
awk 的正则和字符串函数
查看当前环境:
locale
对协议字段、机器生成日志和可复现构建,常见做法是:
LC_ALL=C grep -E '^[A-Z_]+$' file
LC_ALL=C sort file
但这会改变字符分类和排序语义。对于面向用户的自然语言文本,不能简单把 LC_ALL=C 当作普遍正确的选择。
还要区分:
- UTF-8 字符;
- 字节;
- Unicode 组合字符;
- Windows CRLF;
- Unix LF;
- 文件名中的换行或 NUL。
grep、sed、awk 等经典工具通常以换行为主要记录边界。若数据本身允许换行出现在字段内,行式处理模型就不再足够。
十二、与 find、xargs 的边界:文件名不是普通文本行
处理文件列表时,下面的写法存在文件名安全问题:
find . -type f | xargs grep 'ERROR'
原因是:
- 文件名可能包含空格;
- 可能包含制表符或换行;
- 可能以
-开头; - 空输入时,某些
xargs实现仍可能执行一次命令。
更安全的 GNU/Linux 写法是使用 NUL 分隔:
find . -type f -print0 |
xargs -0 grep -H -- 'ERROR'
数据流变为:
find 输出:文件名\0文件名\0文件名\0
xargs -0:按 NUL 而不是空白拆分
grep --:将后续内容视为文件名而不是选项
如果支持 find -exec ... {} +,可以少依赖一个外部解析器:
find . -type f -exec grep -H -- 'ERROR' {} +
find 的 -exec ... {} + 会把多个找到的路径批量传给命令,同时正确处理路径中的空格和换行。若需要并发,GNU xargs 提供:
find . -type f -print0 |
xargs -0 -r -n 20 -P 4 grep -H -- 'ERROR'
但并发会改变输出顺序,也会增加 I/O 竞争;grep 退出状态聚合和错误诊断也更复杂。只有在任务可安全并发、目标文件不会被同时修改,并且确实需要并发时才使用。
不要把:
find . -name '*.log'
中的 *.log 写成未引用形式:
find . -name *.log
前者由 find 解释模式,后者可能先被 Shell 展开,导致参数数量和含义变化。
十三、不要用行式工具解析 JSON、YAML 和复杂 CSV
下面的 JSON:
{"user":"alice","message":"a,b"}
不能可靠地通过:
cut -d, -f2
解析,因为逗号是 JSON 字符串内容的一部分。类似地,使用:
grep '"status":"ok"'
搜索 JSON 也可能因空格、字段顺序、转义或嵌套结构变化而失效。
应使用结构化工具:
jq -r '.user' data.json
jq -r 'select(.status == "ok") | .user' data.json
其工作方式不是“搜索看起来像字段的文本”,而是:
- 词法解析 JSON;
- 验证语法;
- 构造对象和数组;
- 按路径和条件访问值;
- 按指定格式输出。
YAML 同样应使用 YAML 解析器,例如 yq。如果需要更新配置,应采用解析、修改、校验、写回的流程,而不是直接用 sed 替换可能出现在注释、字符串或其他层级中的相同文本。
十四、生产脚本中的验证、权限和恢复
1. 输入验证
不要假设输入格式永远正确:
awk -F: 'NF != 3 {
print "invalid record at line " FNR > "/dev/stderr"
bad=1
next
}
{
print $1, $3
}
END {
exit bad
}' input
这个例子要求每行恰好有三个冒号分隔字段。异常行不会静默进入后续结果,并通过退出状态通知调用者。
2. 权限和符号链接
文本过滤通常只读,但 sed -i、重定向和 mv 会修改文件。需要检查:
- 当前用户是否有目录写权限;
- 目标是否为符号链接;
- 目标文件是否有特殊属主或权限;
- 临时文件是否会被其他用户读取;
- 更新是否需要保留扩展属性或安全标签。
对特权目录中的配置文件,不能仅凭命令成功退出就认为更新正确。至少应执行语法检查、差异检查和服务重新加载后的状态验证。
3. 不要把未验证的文本当作 Shell 代码
例如:
for x in $(cat file); do
...
done
会发生命令替换结果的词拆分和通配符展开,无法安全表示任意文本。读取按行数据应使用:
while IFS= read -r line; do
printf '%s\n' "$line"
done < file
处理任意文件名则优先使用 NUL 分隔接口:
while IFS= read -r -d '' path; do
printf '%s\n' "$path"
done < <(find . -type f -print0)
即使文本来自 grep 或 awk,也不应直接拼接后交给 eval。文本数据和 Shell 代码必须保持边界。
十五、如何选择这些工具
可以按数据模型选择:
- 只判断或筛选行:
grep; - 对行做有限规则替换、删除、打印:
sed; - 需要字段、条件、计数、聚合、格式化:
awk; - 需要按位置或单字符分隔符取字段:
cut; - 需要按字典、数字或字段排序:
sort; - 需要合并相邻重复行或统计连续组:
uniq; - 需要表达匹配规则:正则;
- 需要处理 JSON、YAML、可靠 CSV 或嵌套结构:对应解析器;
- 需要批量处理文件:
find配合-exec或 NUL 安全的xargs。
最容易被忽略的因果关系是:
uniq 只能处理相邻重复
│
▼
要统计全局重复,必须先让相同值相邻
│
▼
通常使用 sort | uniq -c
同样:
cut 只能按固定字段规则切分
│
▼
字段规则若包含引号、转义或嵌套结构
│
▼
切分模型失效,应改用解析器
掌握这些命令的关键,不是记住更多选项,而是先定义输入的记录边界、字段边界、匹配语义、排序关系和错误处理方式。只要这些条件明确,grep、sed、awk、cut、sort、uniq 组成的管道就能保持可解释、可验证,并在超出文本模型时及时让位给结构化工具。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux find 与 xargs:条件、批处理、空字符、安全和并发执行
- 下一篇:Bash 展开与引用深入:变量、命令替换、通配、数组和 IFS
- 延伸:命令行结构化数据:jq、yq、JSON、YAML、流式处理和安全更新
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论