sed 与 awk 编程
# 第 5 章 sed 与 awk 编程
# 目录介绍
# 5.1 sed 基础操作
# 5.1.1 sed 工作流与模式空间
sed = Stream Editor——流编辑器。它不修改原文件(除非加 -i),核心是"读一行 → 处理 → 输出 → 下一行"的流水线:
# sed 工作流:每次读取一行到「模式空间」(pattern space),执行编辑命令,输出结果
sed 's/old/new/' file.txt # 读一行 → 替换 → 输出 → 读下一行
# 关键概念:
# 模式空间 = sed 的"工作台",每次只放一行文本
# 所有编辑操作都作用于模式空间,不会直接修改源文件
# 处理完一行后,模式空间内容自动输出到 stdout
# 查看 sed 对文件的影响(不修改原文件)
sed 's/Hello/HELLO/' file.txt # 只输出到终端,原文件不变
cat file.txt # 文件内容不变!
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
sed 执行流程(5 步循环):
① 读取一行到模式空间
② 按顺序执行所有 sed 命令
③ 命令作用于模式空间
④ 输出模式空间内容 → stdout
⑤ 回到步骤①,直到文件末尾
读一行 → 执行全部命令 → 输出 → 读下一行
(对每一行,所有命令串行执行完毕后才输出一次)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 5.1.2 替换 s 命令——sed 的瑞士军刀
sed 上 80% 的使用场景都是替换,但标志位能极大增强它的能力:
#!/bin/bash
# ===== 基础替换:s/old/new/ ——只替换每行第一个匹配 =====
echo "Hello World, Hello Universe" | sed 's/Hello/Hi/'
# 输出:Hi World, Hello Universe ← 注意:只替换了第一个 Hello!
# ===== 全局替换:g 标志——替换每行所有匹配 =====
echo "Hello World, Hello Universe" | sed 's/Hello/Hi/g'
# 输出:Hi World, Hi Universe ← 全部替换
# ===== 第 n 次匹配:数字标志 =====
echo "aaa aaa aaa" | sed 's/aaa/bbb/2'
# 输出:aaa bbb aaa ← 只替换第 2 个
# ===== 忽略大小写:i 标志 =====
echo "HELLO hello" | sed 's/hello/HI/i'
# 输出:HI HI
# ===== 打印替换行:p 标志(常与 -n 联用)=====
sed -n 's/ERROR/++ERROR++/p' app.log
# 只输出发生替换的行,不输出其他行
# ===== 写入文件:w 标志 =====
sed -n 's/ERROR/BUG/w errors.txt' app.log
# 将发生替换的行写入 errors.txt
# ===== 组合使用标志 =====
sed 's/old/NEW/gi' file.txt # 全局 + 忽略大小写
sed -n 's/error/ERROR/2p' file.txt # 第2次匹配 + 打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
分隔符的灵活使用——当替换内容包含 / 时:
# 路径替换——用 # 或 | 或 _ 作为分隔符
sed 's|/usr/local/bin|/opt/bin|g' path.txt # ✅ 清晰
sed 's#/etc/nginx#/usr/local/nginx#g' conf.sh # ✅ 推荐
# 任何字符都可以做分隔符(紧跟 s 之后的那个字符)
sed 's_apple_orange_g' fruit.txt # 用下划线
1
2
3
4
5
6
2
3
4
5
6
反向引用 & 与 \1——在替换中引用匹配内容:
# & = 整个匹配的文本
echo "I love cats" | sed 's/cats/& and dogs/'
# 输出:I love cats and dogs
# 给匹配内容加括号
echo "Error: timeout at 10:30" | sed 's/Error: .*/[&]/'
# 输出:[Error: timeout at 10:30]
# \1 \2 ... = 捕获组引用
echo "John Smith" | sed -E 's/(\w+) (\w+)/\2, \1/'
# 输出:Smith, John ← 名姓颠倒
# 实战:给电话号码加分隔符
echo "Phone: 13800138000" | sed -E 's/([0-9]{3})([0-9]{4})([0-9]{4})/\1-\2-\3/'
# 输出:Phone: 138-0013-8000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.1.3 行寻址——精准定位编辑目标
sed 默认对每一行执行命令,但行寻址让你只在符合条件的行上执行命令:
#!/bin/bash
# ===== 数字寻址:第 n 行 =====
sed '3s/apple/orange/' file.txt # 只替换第 3 行
sed '2,5s/apple/orange/' file.txt # 替换第 2~5 行
sed '3,$s/apple/orange/' file.txt # 替换第 3 行到最后一行 ($)
# ===== 正则寻址:匹配模式的行 =====
sed '/ERROR/s/INFO/DEBUG/' app.log # 只在含 ERROR 的行中替换 INFO→DEBUG
sed '/^#/s/old/new/' config.conf # 只在注释行中替换
# ===== 范围寻址:从匹配 A 的行到匹配 B 的行 =====
sed '/START/,/END/s/foo/bar/' file.txt # 在 START→END 之间的所有行执行替换
# 注意:范围是"开启后一直持续到关闭",允许多段匹配
# ===== 取反寻址:! 后缀 =====
sed '5!s/foo/bar/' file.txt # 除了第 5 行,其他全替换
sed '/^#/!s/;/ --/g' code.js # 非注释行:替换分号
# ===== 组合多个地址范围 =====
sed '1,10s/foo/bar/; 20,30s/baz/qux/' file.txt # 不同范围做不同替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
实战:行寻址的常用模式:
# 删除第 1 行
sed '1d' file.txt
# 删除最后一行
sed '$d' file.txt
# 删除第 10~20 行
sed '10,20d' file.txt
# 删除空白行
sed '/^$/d' file.txt
# 删除注释行(以 # 开头)
sed '/^#/d' nginx.conf
# 从包含 START 的行删除到包含 END 的行
sed '/START/,/END/d' file.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.1.4 删除 d——按行过滤
#!/bin/bash
# ===== d 命令:删除模式空间内容,立即开始下一行 =====
# 删除操作不输出当前行,直接跳转到下一行的循环
seq 1 10 | sed '5d'
# 输出:1 2 3 4 6 7 8 9 10 ← 第 5 行被删除
seq 1 10 | sed '2,4d'
# 输出:1 5 6 7 8 9 10 ← 删除 2~4 行
# ===== 删除匹配行 =====
sed '/^\s*$/d' file.txt # 删除空行和纯空白行
sed '/^#/d; /^$/d' nginx.conf # 删除注释行 + 空行(干净配置文件)
# ===== 取反删除(保留匹配行)=====
sed '/ERROR/!d' app.log # 等价于 grep "ERROR"——只保留错误行
# ===== 范围删除 =====
sed '/<!--/,/-->/d' index.html # 删除 HTML 注释
sed '/BEGIN CERTIFICATE/,/END CERTIFICATE/d' cert.pem # 删除证书内容块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 5.1.5 插入/追加/替换整行 i/a/c
#!/bin/bash
# ===== i —— 在匹配行「之前」插入 (insert) =====
seq 1 5 | sed '3i\--- 插入行 ---'
# 输出:
# 1
# 2
# --- 插入行 --- ← 在第 3 行之前插入
# 3
# 4
# 5
# ===== a —— 在匹配行「之后」追加 (append) =====
seq 1 5 | sed '3a\--- 追加行 ---'
# 输出:
# 1
# 2
# 3
# --- 追加行 --- ← 在第 3 行之后追加
# 4
# 5
# ===== c —— 替换整行 (change) =====
seq 1 5 | sed '3c\--- 替换整行 ---'
# 输出:
# 1
# 2
# --- 替换整行 --- ← 整个第 3 行被替换
# 4
# 5
# ===== 在文件首行/尾行插入 =====
sed '1i\#!/bin/bash' script.sh # 文件开头插入 shebang
sed '$a\# END OF FILE' script.sh # 文件末尾追加标记
# ===== 多行插入:每行后面加 \ =====
seq 1 3 | sed '2a\line A\nline B'
# 等价于:
seq 1 3 | sed '2a\line A\
line B'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 5.1.6 打印 p 与静默模式 -n
#!/bin/bash
# ===== 默认行为:每行都输出 =====
# 不加 -n 时,p 命令会导致匹配行输出「两次」:
# 一次来自 p 命令,一次来自默认输出
seq 1 5 | sed '3p'
# 输出:1 2 3 3 4 5 ← 第 3 行出现两次!
# ===== -n + p:只输出指定行 =====
seq 1 5 | sed -n '3p' # 只打印第 3 行
# 输出:3
seq 1 5 | sed -n '2,4p' # 打印第 2~4 行
# 输出:2 3 4
# ===== 通用模式:sed -n '/pattern/p' = grep =====
sed -n '/ERROR/p' app.log # 等价于 grep "ERROR" app.log
# ===== 打印行号:= 命令 =====
sed -n '/ERROR/=' app.log # 打印匹配行的行号(只显示数字)
sed -n '/ERROR/{=;p}' app.log # 先打行号,再打印该行内容
# ===== 实战:提取函数体 =====
# 提取 Python 函数 def foo(): 到 return 之间的所有行
sed -n '/^def foo()/,/^ return/p' script.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 5.1.7 实战:一行搞定常见文本编辑
#!/bin/bash
# ===== 1. 去掉行首/行尾空白 =====
sed 's/^[[:space:]]*//; s/[[:space:]]*$//' file.txt
# ===== 2. 给每行加行号 =====
sed = file.txt | sed 'N; s/\n/\t/' # 行号 + tab + 内容
# 输出格式:1\t内容第一行
# ===== 3. 删除空行和只有空格的行 =====
sed '/^[[:space:]]*$/d' file.txt
# ===== 4. 在每行末尾添加内容 =====
sed 's/$/ END/' file.txt
# ===== 5. 提取括号中的内容 =====
echo "name: Alice (eng) (30)" | sed -E 's/.*\((.*)\).*/\1/'
# 输出:30(贪婪匹配,取最后一个括号内容)
# ===== 6. 文件就地修改:-i 选项 =====
sed -i 's/old/new/g' file.txt # 直接修改文件!
sed -i '.bak' 's/old/new/g' file.txt # macOS: 备份为 file.txt.bak
sed -i.bak 's/old/new/g' file.txt # Linux: 备份为 file.txt.bak
# ⚠️ -i 是破坏性操作,建议先不加 -i 预览效果
# 确认无误后再加 -i 或 -i.bak 真正修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 5.2 sed 高级编辑
# 5.2.1 多点编辑 -e 与脚本文件 -f
当一条语句要执行多个编辑操作时:
#!/bin/bash
# ===== -e:多次指定编辑命令 =====
sed -e 's/foo/bar/' -e 's/baz/qux/' file.txt # 顺序执行两个替换
# ===== 分号 : 串联多个命令 =====
sed 's/foo/bar/; s/baz/qux/; s/aaa/bbb/' file.txt # 等价但更简洁
# ===== {} 分组:对同一地址执行多个命令 =====
sed '/ERROR/{
s/INFO/DEBUG/
s/WARN/ERROR/
s/^/[PREFIX] /
}' app.log
# 只在含 ERROR 的行上执行三个替换
# ===== -f:从文件加载脚本(复杂脚本必备)=====
cat > sed_script.sed << 'EOF'
# 这是 sed 脚本文件
/^#/d # 删除注释行
/^$/d # 删除空行
s/old/new/g # 全局替换
EOF
sed -f sed_script.sed file.txt # 执行脚本文件
# ===== 实战:美化 JSON/代码的批量编辑脚本 =====
cat > clean_code.sed << 'EOF'
# 删除行尾空白
s/[[:space:]]*$//
# 删除连续空行(压缩为一个)
/^$/{
N
/^\n$/d
}
# 统一缩进:将 4 个空格替换为 tab
s/ /\t/g
EOF
sed -f clean_code.sed source.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 5.2.2 保持空间 h/H/g/G/x——双缓冲区的魔法
这是 sed 的"黑魔法"——模式空间之外还有一个保持空间 (hold space),二者可以相互拷贝/追加:
#!/bin/bash
# ===== 两个缓冲区的关系 =====
# 模式空间 (pattern space) = 工作区,读入每一行
# 保持空间 (hold space) = 暂存区,初始为空
# h = 拷贝模式空间 → 保持空间(覆盖)
# H = 追加模式空间 → 保持空间(追加 + 换行)
# g = 拷贝保持空间 → 模式空间(覆盖)
# G = 追加保持空间 → 模式空间(追加 + 换行)
# x = 交换两个空间的内容
# ===== 核心应用 1:反转文件行顺序 =====
seq 1 5 | sed -n '1!G; h; $p'
# 输出:5 4 3 2 1
# 执行过程(逐行):
# 读第1行(1): 1!G→跳过; h→保持空间=1
# 读第2行(2): G→模式空间=2\n1; h→保持空间=2\n1
# 读第3行(3): G→模式空间=3\n2\n1; h→保持空间=3\n2\n1
# ...
# 读最后行(5): G→模式空间=5\n4\n3\n2\n1; $p→打印
# ===== 核心应用 2:用空白行分隔的段落反转 =====
# 将每个段落内部的行反转
sed '/^$/{ # 遇到空行时
x # 交换空间
s/^\n// # 去除前导空行
p # 打印保持空间(已反转的段落)
s/.*// # 清空
x # 交换回来
}
/^$/!{ # 非空行
H # 追加到保持空间
d # 删除模式空间
}' file.txt
# ===== 核心应用 3:相邻行合并 =====
seq 1 6 | sed 'N; s/\n/ /'
# 输出:1 2 3 4 5 6
# N = 读下一行追加到模式空间(用 \n 连接)
# ===== G 的经典用法:行间插入空行 =====
seq 1 5 | sed 'G'
# 输出:1\n\n2\n\n3\n\n4\n\n5\n\n
# G 在每个模式空间后追加保持空间(初始为空→追加空行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
保持空间速查表:
| 命令 | 方向 | 效果 | 记忆 |
|---|---|---|---|
h | 模式→保持 | 拷贝(覆盖) | hold |
H | 模式→保持 | 追加 +\n | Huge append |
g | 保持→模式 | 拷贝(覆盖) | get |
G | 保持→模式 | 追加 +\n | Get append |
x | 双向 | 交换 | exchange |
# 5.2.3 流程控制:标签与跳转
sed 有 goto 式流程控制,配合 t (条件跳转) 和 b (无条件跳转):
#!/bin/bash
# ===== :label —— 定义标签 =====
# b label —— 无条件跳转到 label
# t label —— 如果上一个 s 替换成功,则跳转(类似 if 替换成功 then goto)
# ===== 实战 1:循环替换直到没有匹配 =====
echo "a b c" | sed ':loop; s/ / /g; t loop'
# 输出:a b c(将所有连续空格压缩为单个空格)
# 解释:
# :loop → 标记循环起点
# s/ / /g → 将两个空格替换为一个空格
# t loop → 如果上面替换成功则跳回 loop(继续压缩)
# ===== 实战 2:给每行加行号(右对齐)=====
sed = file.txt | sed 'N; s/\n/ /' | sed ':a; s/^\( \{1,9\}\) \([0-9]\)/\1\2/; ta'
# 如果行号小于 10 位则前面补空格,实现数字右对齐
# ===== 实战 3:无条件跳转删除行! =====
sed '/skip/,/resume/b; s/delete/REMOVED/' file.txt
# 在 skip→resume 之间的行跳过替换(b 后面没标签 = 跳到末尾 = 跳过后续命令)
# 等价于:取反行寻址
sed '/skip/,/resume/!s/delete/REMOVED/' file.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 5.2.4 多行处理 N/P/D
sed 默认逐行处理,但 N/P/D 让你一次处理多行:
#!/bin/bash
# ===== N —— 读下一行追加到模式空间(并非"下一行替换")=====
seq 1 6 | sed 'N; s/\n/ → /'
# 输出:
# 1 → 2
# 3 → 4
# 5 → 6
# 解释:N 每次读两行,两行两行地处理
# ===== P —— 打印模式空间中第一个换行之前的内容 =====
# (p 打印整个模式空间,P 只打印第一行)
seq 1 4 | sed -n 'N; P'
# 输出:1 3
# N 读入 1\n2 → P 只打印 1 → N 读入 3\n4 → P 只打印 3
# ===== D —— 删除模式空间中第一个换行之前的内容,然后重新开始循环 =====
# (d 删除整个模式空间并开始下一行,D 只删除第一行但不读新行)
sed '/^$/{N; /^\n$/D}' file.txt
# 删除连续空行:读到空行后读下一行,如果也是空行则删除第一个
# ===== 实战:连接以 \ 结尾的续行 =====
# 将:
# line1 \
# line2 \
# line3
# 转换为:
# line1 line2 line3
sed ':a; /\\$/ { N; s/\\\n//; ta }' file.txt
# 解释:如果行以 \ 结尾 → 读下一行 → 删除 \ 和换行 → 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
多行命令速查:
| 命令 | 含义 | vs 单行版 |
|---|---|---|
N | 读下一行追加到模式空间 | 模式空间可能有多行 |
P | 打印模式空间第一行 | p 打印全部 |
D | 删除模式空间第一行,不读新行重新循环 | d 删除全部并读新行 |
# 5.2.5 实战:批量修改配置文件
#!/bin/bash
# ===== 场景:批量修改 Nginx 配置 =====
cat nginx.conf
# 需求 1:修改监听端口 80 → 8080
sed -i.bak 's/listen 80;/listen 8080;/' nginx.conf
# 需求 2:在所有非注释行添加注释说明
sed -i '/^[[:space:]]*#/! s/^/ # [CUSTOM] /' nginx.conf
# ===== 场景:批量修改 Dockerfile 版本号 =====
sed -i 's/FROM node:[0-9.]*/FROM node:20.11/' Dockerfile
# ===== 场景:提取配置文件中的所有参数名和值 =====
sed -n 's/^\s*\([A-Za-z_][A-Za-z0-9_]*\)\s*=\s*\(.*\)/\1 = \2/p' config.ini
# ===== 场景:批量加解密替换(从映射文件) =====
while IFS='=' read -r old new; do
sed -i "s/$old/$new/g" target.txt
done < mapping.txt
# ⚠️ 如果 $old 含正则元字符,需要先转义:old_escaped=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$old")
# ===== 场景:给 Markdown 代码块添加语言标记 =====
# 将所有 ``` 替换为 ```bash(如果上一行不是已经有语言标记的)
sed -E '/```$/{ N; /```\n$/!b; s/```\n/```bash\n/ }' README.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 5.2.6 sed 新手陷阱
陷阱 1:-i 直接修改不可逆
# ❌ 危险——没有备份
sed -i 's/old/new/' important.conf # 改错了无法恢复!
# ✅ 安全——先预览再改
sed 's/old/new/' important.conf | diff important.conf - # 先 diff 看差异
sed -i.bak 's/old/new/' important.conf # 带备份
1
2
3
4
5
6
2
3
4
5
6
陷阱 2:正则贪婪匹配
echo "before <tag>content</tag> after" | sed 's/<.*>//'
# 输出:before after ← 把 <tag>content</tag> 全删了!
# ✅ 非贪婪匹配(sed 没有非贪婪,用字符集)
echo "before <tag>content</tag> after" | sed 's/<[^>]*>//g'
# 输出:before content after
1
2
3
4
5
6
2
3
4
5
6
陷阱 3:$ 在替换中的歧义
# sed 中 $ 只匹配行尾,不是正则的后行断言
echo "price: $100" | sed 's/\$100/$50/'
# ❌ sed 解释:匹配字面量 $100?双层转义让人头大
# ✅ 用单引号防止 Shell 展开
echo 'price: $100' | sed 's/\$100/$50/'
# 输出:price: $50
1
2
3
4
5
6
7
2
3
4
5
6
7
陷阱 4:macOS 的 sed 是 BSD 版
# macOS 的 -i 必须带备份后缀(可以为空,但必须写)
sed -i '' 's/old/new/g' file.txt # macOS
sed -i 's/old/new/g' file.txt # Linux
# macOS sed 不支持 \t \n 等转义
# 解决办法:用 $'\t' 或按 Ctrl+V Tab
sed "s/ /$'\t'/g" file.txt # macOS 换 tab
1
2
3
4
5
6
7
2
3
4
5
6
7
# 5.3 awk 基础入门
# 5.3.1 awk 工作模型(记录/字段)
awk 是一个完整的文本处理编程语言——按"记录(行) → 字段(列)"模型工作:
# awk 工作模型:逐行读取,自动分割字段
# 记录 (Record) = 一行文本(默认 RS="\n")
# 字段 (Field) = 一列文本(默认 FS=" " 或 "\t")
awk '{print $1, $3}' data.txt
# $0 = 整行,$1 = 第1字段,$2 = 第2字段,...,$NF = 最后一个字段
# ===== awk 执行流程(3 段)=====
# BEGIN { } ← 开始前执行一次
# pattern { action } ← 每行:满足 pattern 时执行 action
# END { } ← 结束后执行一次
# ===== 最小 awk 程序 =====
awk '{print}' file.txt # 等价于 cat(直接输出每行)
# ===== 最常用的写法 =====
awk '{print $1}' access.log # 打印每行第 1 列
awk '$3 > 100' data.txt # 第 3 列大于 100 的行(默认动作=print)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5.3.2 模式-动作结构——awk 的核心范式
#!/bin/bash
# ===== 模式可以是:正则、条件表达式、范围、BEGIN/END =====
# 正则模式
awk '/ERROR/ {print $0}' app.log # 打印含 ERROR 的行
# 条件表达式
awk '$3 > 500 {print $1, $3}' data.txt # 第 3 列 > 500 时打印第 1+3 列
# 范围模式(类似 sed)
awk '/START/,/END/ {print}' file.txt # 从 START 行到 END 行
# 空模式 = 每行都执行
awk '{sum += $2} END{print sum}' data.txt # 每行累加第2列
# 空动作 = 默认 print $0
awk '$3 > 100' data.txt # 默认打印匹配行
# ===== 复合条件:&& || ! =====
awk '$3 > 100 && $4 == "ERROR"' app.log # 两个条件同时满足
awk '$1 == "GET" || $1 == "POST"' access.log # 任一条件
awk '! /^#/' config.conf # 非注释行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.3.3 内建变量 NR/NF/FS/OFS/RS/ORS
#!/bin/bash
# ===== NR = Number of Records —— 当前行号(跨文件累加)=====
awk '{print NR, $0}' file.txt # 带行号打印
awk 'NR==1 {print "HEAD:", $0}' file.txt # 只处理第一行
awk 'NR>=10 && NR<=20' file.txt # 只处理第 10~20 行
# ===== NF = Number of Fields —— 当前行的字段数 =====
awk '{print NF, $0}' data.txt # 每行列数 + 内容
awk 'NF > 5' data.txt # 字段数大于 5 的行
awk '{print $NF}' data.txt # 打印最后一列(动态!)
# ===== FS = Field Separator —— 输入字段分隔符(默认空白)=====
awk -F ':' '{print $1, $7}' /etc/passwd # 用冒号分割
awk -F '[,;]' '{print $1, $3}' data.csv # 多字符分隔
awk 'BEGIN{FS=":"} {print $1}' /etc/passwd # 在 BEGIN 中指定
# ===== OFS = Output Field Separator —— 输出字段分隔符 =====
# (用逗号分隔时,OFS 会插入)
awk 'BEGIN{OFS=" | "} {print $1, $2, $3}' data.txt
# 输出:col1 | col2 | col3
# ===== RS = Record Separator —— 输入记录分隔符(默认 \n)=====
# 处理段落模式(记录以空行为分隔)
awk 'BEGIN{RS=""} {print $1, $2}' file.txt
# 每个"段落"是一条记录
# ===== ORS = Output Record Separator —— 输出记录分隔符(默认 \n)=====
awk 'BEGIN{ORS=" "} {print $0}' file.txt # 把所有行连成一行
# ===== 实战速查:用 NR/NF 提取最后一行/最后一列 =====
awk 'END{print}' file.txt # 最后一行
awk '{last=$0} END{print last}' file.txt # 同上
awk 'NR==1{print}' file.txt # 第一行
awk '{print $(NF-1)}' data.txt # 倒数第二列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
内建变量速查表:
| 变量 | 含义 | 默认值 |
|---|---|---|
NR | 已处理的记录数(跨文件) | 从 1 开始 |
FNR | 当前文件的记录数(每文件重置) | 每文件从 1 开始 |
NF | 当前记录的字段数 | — |
FS | 输入字段分隔符 | 空白 [ \t]+ |
OFS | 输出字段分隔符 | 空格 " " |
RS | 输入记录分隔符 | "\n" |
ORS | 输出记录分隔符 | "\n" |
FILENAME | 当前处理的文件名 | — |
# 5.3.4 BEGIN/END 块——前置处理与收尾
#!/bin/bash
# ===== BEGIN 块:处理开始前的准备工作 =====
awk 'BEGIN {
print "===== 报表开始 ====="
print "时间:", strftime()
printf "%-20s %10s\n", "项目", "金额"
}'
# ===== END 块:处理结束后的汇总输出 =====
awk '{sum += $3}
END {
print "总金额:", sum
print "平均:", sum/NR
}' sales.txt
# ===== BEGIN + END = 完整的报表程序 =====
awk 'BEGIN {
FS=","; OFS="\t"
print "名称", "数量", "单价", "小计"
print "--------------------------------"
}
{
subtotal = $2 * $3
total += subtotal
printf "%-10s %6d %8.2f %10.2f\n", $1, $2, $3, subtotal
}
END {
print "--------------------------------"
printf "总计:%35.2f\n", total
}' products.csv
# ===== BEGIN 中自定义变量 =====
awk 'BEGIN {
threshold = 100 # 阈值
count = 0 # 计数器
}
$3 > threshold {count++}
END {
print "超过阈值", threshold, "的记录数:", count
}' data.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 5.3.5 字段分割与条件过滤
#!/bin/bash
# ===== -F 选项:指定分隔符 =====
awk -F ':' '{print $1, $3}' /etc/passwd # 冒号分割
awk -F '[,;|]' '{print $1}' data.txt # 多字符分割(正则字符集)
awk -F '"[^"]*"|[^,]+' '{print $1}' log.csv # 复杂 CSV 解析
# ===== 条件过滤 =====
awk '$3 > 1000' sales.txt # 数值比较
awk '$2 == "ERROR"' app.log # 字符串匹配(精确)
awk '$1 ~ /^[0-9]+$/' data.txt # 正则匹配 (~)
awk '$1 !~ /^#/' config.conf # 不匹配 (!~)
awk 'tolower($4) ~ /error/' app.log # 忽略大小写正则
# ===== 多条件组合过滤 =====
awk '$1 == "nginx" && $3 > 500' ps.log # 且
awk '$1 == "GET" || $1 == "POST"' access.log # 或
awk 'NR > 1 && NF >= 3' data.csv # 跳过标题行 + 至少 3 列
awk 'NR % 2 == 0' file.txt # 偶数行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 5.3.6 格式化输出 printf
#!/bin/bash
# printf 格式:%[flags][width][.precision]type
# %s = 字符串, %d = 整数, %f = 浮点, %x = 十六进制
# ===== 表格输出 =====
awk 'BEGIN {
printf "%-15s %8s %8s\n", "Name", "Score", "Rank"
printf "%-15s %8s %8s\n", "----", "-----", "----"
}
{
printf "%-15s %8.2f %8d\n", $1, $2, NR # 左对齐15宽, 浮点2位, 整数
}' scores.txt
# ===== 十六进制输出 =====
echo "255" | awk '{printf "0x%02X\n", $1}'
# 输出:0xFF
# ===== 实战:格式化 Nginx 访问日志 =====
awk '{
printf "[%s] %-6s %-30s → HTTP %d, %d bytes\n",
$4, $6, $7, $9, $10
}' access.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.4 awk 高级编程
# 5.4.1 关联数组——awk 最强大的数据结构
关联数组 = 键值对 + 自动创建。无需声明,直接用:
#!/bin/bash
# ===== 计数统计:统计 IP 访问次数 =====
awk '{count[$1]++}
END {
for (ip in count) {
print ip, count[ip]
}
}' access.log
# ===== 累加汇总:按类别汇总金额 =====
awk '{total[$2] += $3}
END {
for (cat in total) {
printf "%-15s %10.2f\n", cat, total[cat]
}
}' sales.txt
# ===== 分组拼接:按用户收集所有操作 =====
awk '{actions[$1] = actions[$1] ", " $2}
END {
for (user in actions) {
# 去掉开头的 ", "
sub(/^, /, "", actions[user])
print user ":", actions[user]
}
}' user_actions.log
# ===== if (key in array) —— 检查键是否存在 =====
awk '{
if (!($1 in seen)) { # 首次出现的 IP
print $0
seen[$1] = 1
}
}' access.log # 等价于 sort -u -k1
# ===== delete array[key] —— 删除元素 =====
awk '{seen[$1]++}
END {
delete seen["127.0.0.1"] # 删除本地 IP 统计
for (ip in seen) print ip, seen[ip]
}' access.log
# ===== 统计每个状态码的出现次数并排序 =====
awk '{status[$9]++}
END {
for (code in status) {
print status[code], code # 打印:次数 状态码
}
}' access.log | sort -rn # 管道给 sort 按次数降序
# ===== 多维数组模拟 (awk 没有真正多维,用下标拼接) =====
awk '{matrix[$1","$2] = $3}
END {
for (key in matrix) {
split(key, parts, ",")
print "Row", parts[1], "Col", parts[2], "=", matrix[key]
}
}' data.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 5.4.2 循环与分支——awk 是一门编程语言
#!/bin/bash
# ===== if / else if / else =====
awk '{
if ($3 >= 90) grade = "A"
else if ($3 >= 80) grade = "B"
else if ($3 >= 70) grade = "C"
else grade = "D"
print $1, $3, grade
}' scores.txt
# ===== for 循环:遍历字段 =====
awk '{
for (i = 1; i <= NF; i++) {
if ($i ~ /^[0-9]+$/) sum += $i # 累加所有数字字段
}
}
END {print "Total:", sum}' data.txt
# ===== for ... in:遍历关联数组 =====
awk '{word[$1]++}
END {
for (w in word) { # w 是键
print w, word[w] # word[w] 是值
}
}' words.txt
# ⚠️ 遍历顺序不确定(关联数组无序)
# ===== while 循环:处理字段中的子项 =====
awk '{
i = 1
while (i <= NF) {
print "Field", i, "=", $i
i++
}
}' data.txt
# ===== do...while =====
awk 'BEGIN {
i = 1
do {
print i * i
i++
} while (i <= 5)
}'
# ===== 控制流:break / continue / next / exit =====
awk '{
if (NF == 0) next # 跳过空行(continue 到下一行)
if ($1 == "STOP") exit # 遇到 STOP 直接退出
if ($1 ~ /^#/) next # 跳过注释行
if ($3 < 0) { # 遇到负数只处理到这一行为止
print "WARN:", $0
nextfile # 跳到下一个文件(gawk)
}
print $0
}' *.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
流程控制速查:
| 关键词 | 作用 |
|---|---|
next | 跳过当前行,开始下一行(类似 continue) |
nextfile | 跳过当前文件的剩余行 |
exit | 立即退出 awk(但会执行 END 块) |
exit 1 | 退出并返回状态码 1 |
break | 跳出当前循环 |
continue | 跳过本次循环剩余语句 |
# 5.4.3 内置函数速查(字符串/数学/时间)
#!/bin/bash
# ===== 字符串函数 =====
awk '{
# length(str) —— 字符串长度
print length($0)
# substr(str, start, len) —— 子串(start 从 1 开始!)
print substr($0, 1, 10) # 前 10 个字符
# index(str, search) —— 查找位置(找不到返回 0)
print index($0, "ERROR")
# match(str, regex) —— 正则匹配位置(设置 RSTART/RLENGTH)
if (match($0, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) {
ip = substr($0, RSTART, RLENGTH)
}
# split(str, arr, sep) —— 分割字符串到数组
n = split("a,b,c", parts, ",") # parts[1]="a" ... n=3
# sub(regex, repl, target) —— 替换第一个匹配(原地修改)
sub(/^ +/, "", $0) # 去行首空格
# gsub(regex, repl, target) —— 全局替换
gsub(/,/, "|", $0) # 所有逗号变竖线
# tolower() / toupper()
print tolower($1), toupper($2)
# sprintf(format, ...) —— 格式化到字符串
line = sprintf("%-10s %6.2f", $1, $2)
}'
# ===== 数学函数 =====
awk '{
print int(3.14) # 3 (取整)
print sqrt(16) # 4
print sin(0) # 三角函数(弧度)
print exp(1) # e
print log(2.718) # 自然对数
print rand() # [0,1) 随机数
print srand() # 设置随机种子
}'
# ===== 时间函数 (gawk) =====
awk 'BEGIN {
print strftime() # 当前时间字符串
print strftime("%Y-%m-%d %H:%M:%S") # 格式化时间
print systime() # Unix 时间戳(秒)
}'
# ===== 实战:解析 Nginx 日志时间 =====
awk '{
# $4 格式:[10/Oct/2023:13:55:36
split($4, t, "/")
month_str = t[2]
# 月份缩写 → 数字
months["Jan"]="01"; months["Feb"]="02"; months["Mar"]="03"
# ...
print t[3], "-", months[month_str], "-", t[1]
}' access.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 5.4.4 多文件处理 FILENAME/FNR 与 nextfile
#!/bin/bash
# ===== 同时处理多个文件 =====
awk '{print FILENAME, FNR, $0}' file1.txt file2.txt
# FILENAME = 当前文件名
# FNR = 当前文件内的行号(每切换文件就重置)
# NR = 全局行号(跨文件累加)
# ===== 判断文件边界:检测第一个文件是否处理完 =====
awk 'FNR == 1 && NR > 1 {
print "===== 文件分隔线 ====="
}
{print $0}' file1.txt file2.txt
# ===== 根据文件名做不同处理 =====
awk 'FILENAME ~ /\.log$/ {logs[FNR] = $0}
FILENAME ~ /\.csv$/ {csv[$1] = $2}
END {
# 合并处理两个文件的数据
}' access.log data.csv
# ===== 两个文件关联 Join =====
# 文件1 (users.txt): uid name
# 文件2 (scores.txt): uid score
awk 'NR == FNR {
# 处理第一个文件时 NR == FNR
name[$1] = $2
next
}
{
# 处理第二个文件时 NR > FNR
print $1, name[$1], $2
}' users.txt scores.txt
# 输出格式:uid name score(JOIN 效果)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 5.4.5 实战:日志统计报表
#!/bin/bash
# ============================================
# 场景 1:Nginx 访问日志分析报表
# ============================================
cat > nginx_report.awk << 'AWKEOF'
BEGIN {
FS = " "
print "==========================================="
print " Nginx 访问日志分析报表"
print " 生成时间:", strftime("%Y-%m-%d %H:%M:%S")
print "==========================================="
}
{
# 统计 HTTP 状态码
status[$9]++
# 统计 IP 访问次数
ip[$1]++
# 统计请求方法
method[$6]++
# 统计流量(字节)
total_bytes += $10
# 统计 URL 访问次数
url[$7]++
# 记录异常状态码(4xx/5xx)
if ($9 ~ /^[45]/) {
errors[NR] = $1 " → " $7 " [" $9 "]"
error_count++
}
}
END {
printf "\n【总体统计】\n"
printf " 总请求数: %d\n", NR
printf " 总流量: %.2f MB\n", total_bytes / 1024 / 1024
printf " 错误请求: %d (%.1f%%)\n", error_count, (error_count/NR)*100
printf "\n【请求方法分布】\n"
for (m in method) printf " %-6s: %d\n", m, method[m]
printf "\n【状态码分布】\n"
for (s in status) printf " HTTP %s: %d\n", s, status[s]
printf "\n【Top 10 访问 IP】\n"
n = 0
# awk 原生不支持排序,需借助 asort 或管道(这里用简单取最大值法)
printf " %-20s %s\n", "IP 地址", "访问次数"
for (i = 1; i <= 10 && n < length(ip); i++) {
max = 0; max_ip = ""
for (k in ip) {
if (ip[k] > max) { max = ip[k]; max_ip = k }
}
if (max > 0) {
printf " %-20s %d\n", max_ip, max
delete ip[max_ip]
n++
}
}
printf "\n【最近 5 个错误】\n"
count = 0
for (i = NR; i >= 1 && count < 5; i--) {
if (i in errors) {
printf " [行 %d] %s\n", i, errors[i]
count++
}
}
printf "\n===========================================\n"
}
AWKEOF
awk -f nginx_report.awk /var/log/nginx/access.log
# ============================================
# 场景 2:系统资源监控汇总
# ============================================
# 汇总 top 输出中的 CPU/内存
ps aux | awk '
BEGIN {
printf "%-20s %8s %8s %8s\n", "进程", "CPU%", "MEM%", "RSS(MB)"
printf "%-20s %8s %8s %8s\n", "----", "----", "----", "-------"
}
NR > 1 {
cpu_total += $3
mem_total += $4
count++
printf "%-20s %8.1f %8.1f %8.1f\n", $11, $3, $4, $6/1024
}
END {
printf "%-20s %8s %8s %8s\n", "----", "----", "----", "-------"
printf "%-20s %8.1f %8.1f %8s\n", "总计(" count "进程)", cpu_total, mem_total, ""
}'
# ============================================
# 场景 3:日志关键词时序统计
# ============================================
awk '{
# 提取时间戳中的小时 (假设在 $2)
split($2, t, ":")
hour = t[1]
if ($0 ~ /ERROR/) errors[hour]++
if ($0 ~ /WARN/) warns[hour]++
if ($0 ~ /INFO/) infos[hour]++
}
END {
print "Hour\tERROR\tWARN\tINFO"
for (h = 0; h <= 23; h++) {
printf "%02d:00\t%d\t%d\t%d\n",
h, errors[h]+0, warns[h]+0, infos[h]+0
}
}' app.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# 5.4.6 awk 新手陷阱与思考题
陷阱 1:$0 和 $1 取字段——修改字段后 $0 自动重建
echo "a b c" | awk '{$2="X"; print}'
# 输出:a X c
# 修改 $2 后 $0 自动用 OFS 重新拼接(默认 OFS=" ")
# ⚠️ 原来 "a b c" (多空格) 会变成 "a X c" (单空格)
echo "a b c" | awk '{$1=$1; print}'
# 输出:a b c(所有空白被 OFS 归一化)
# $1=$1 是一个技巧——强制 awk 重新计算字段,并用 OFS 重建 $0
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
陷阱 2:关联数组遍历顺序不确定
awk '{a[$1]++} END {for (k in a) print k, a[k]}' data.txt
# ❌ 输出顺序是随机的(哈希表顺序)
# ✅ 需要排序时管道给 sort:
awk '{a[$1]++} END {for (k in a) print a[k], k}' data.txt | sort -rn
1
2
3
4
2
3
4
陷阱 3:数字/字符串自动转换
echo "00123" | awk '{print $1 + 1}' # 输出:124(数字运算)
echo "00123" | awk '{print $1 "x"}' # 输出:00123x(字符串拼接)
# awk 自动判断上下文:算术上下文=数字,字符串上下文=字符串
# ⚠️ 比较时可能出问题
awk '$1 == "01"' file.txt # 字符串比较:"01" == "01"
awk '$1 == 01' file.txt # 数字比较:"01" == 1 (数值上相等)
1
2
3
4
5
6
7
2
3
4
5
6
7
陷阱 4:next 与 getline 的区别
# next = 跳到下一条"记录"(即下一行)
awk '{if (NR==1) next; print}' file.txt # 跳过第一行
# getline = 手动读取下一行(谨慎使用!)
awk '{getline next_line; print $0, next_line}' file.txt
# ⚠️ getline 改变了 NR/FNR,容易搞混。能用 N 就用 N
1
2
3
4
5
6
2
3
4
5
6
思考题:
- sed 反转文件:
seq 1 5 | sed -n '1!G; h; $p'中每一步模式空间和保持空间分别是什么? - awk 分组 TopK:如何用 awk 统计日志中"每个 IP 访问最多的 3 个 URL"?
- sed 递归替换:
sed ':a; s/\b\([0-9]\+\)0\b/ \1 /; ta'的作用是什么? - 跨行匹配:如何用 sed 删除 C 语言风格的多行注释
/* ... */? - awk 实现 uniq -c:如何只用 awk 实现
sort | uniq -c | sort -rn的效果?(提示:关联数组 + END 遍历输出)
上次更新: 2026/06/17, 12:47:39