编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
    • 流程控制与函数
    • 数据与 IO 处理
    • grep 搜索实战
    • sed 与 awk 编程
      • 5.1 sed 基础操作
        • 5.1.1 sed 工作流与模式空间
        • 5.1.2 替换 s 命令——sed 的瑞士军刀
        • 5.1.3 行寻址——精准定位编辑目标
        • 5.1.4 删除 d——按行过滤
        • 5.1.5 插入/追加/替换整行 i/a/c
        • 5.1.6 打印 p 与静默模式 -n
        • 5.1.7 实战:一行搞定常见文本编辑
      • 5.2 sed 高级编辑
        • 5.2.1 多点编辑 -e 与脚本文件 -f
        • 5.2.2 保持空间 h/H/g/G/x——双缓冲区的魔法
        • 5.2.3 流程控制:标签与跳转
        • 5.2.4 多行处理 N/P/D
        • 5.2.5 实战:批量修改配置文件
        • 5.2.6 sed 新手陷阱
      • 5.3 awk 基础入门
        • 5.3.1 awk 工作模型(记录/字段)
        • 5.3.2 模式-动作结构——awk 的核心范式
        • 5.3.3 内建变量 NR/NF/FS/OFS/RS/ORS
        • 5.3.4 BEGIN/END 块——前置处理与收尾
        • 5.3.5 字段分割与条件过滤
        • 5.3.6 格式化输出 printf
      • 5.4 awk 高级编程
        • 5.4.1 关联数组——awk 最强大的数据结构
        • 5.4.2 循环与分支——awk 是一门编程语言
        • 5.4.3 内置函数速查(字符串/数学/时间)
        • 5.4.4 多文件处理 FILENAME/FNR 与 nextfile
        • 5.4.5 实战:日志统计报表
        • 5.4.6 awk 新手陷阱与思考题
    • 文件查找与统计
    • 日志监控与告警
    • 备份进程与磁盘
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
    • 性能与打包分发
  • 工具脚本

  • ScriptHub
  • Shell-Bash
杨充
2018-12-24
目录

sed 与 awk 编程

# 第 5 章 sed 与 awk 编程

# 目录介绍

  • 5.1 sed 基础操作
    • 5.1.1 sed 工作流与模式空间
    • 5.1.2 替换 s 命令——sed 的瑞士军刀
    • 5.1.3 行寻址——精准定位编辑目标
    • 5.1.4 删除 d——按行过滤
    • 5.1.5 插入/追加/替换整行 i/a/c
    • 5.1.6 打印 p 与静默模式 -n
    • 5.1.7 实战:一行搞定常见文本编辑
  • 5.2 sed 高级编辑
    • 5.2.1 多点编辑 -e 与脚本文件 -f
    • 5.2.2 保持空间 h/H/g/G/x——双缓冲区的魔法
    • 5.2.3 流程控制:标签与跳转
    • 5.2.4 多行处理 N/P/D
    • 5.2.5 实战:批量修改配置文件
    • 5.2.6 sed 新手陷阱
  • 5.3 awk 基础入门
    • 5.3.1 awk 工作模型(记录/字段)
    • 5.3.2 模式-动作结构——awk 的核心范式
    • 5.3.3 内建变量 NR/NF/FS/OFS/RS/ORS
    • 5.3.4 BEGIN/END 块——前置处理与收尾
    • 5.3.5 字段分割与条件过滤
    • 5.3.6 格式化输出 printf
  • 5.4 awk 高级编程
    • 5.4.1 关联数组——awk 最强大的数据结构
    • 5.4.2 循环与分支——awk 是一门编程语言
    • 5.4.3 内置函数速查(字符串/数学/时间)
    • 5.4.4 多文件处理 FILENAME/FNR 与 nextfile
    • 5.4.5 实战:日志统计报表
    • 5.4.6 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

sed 执行流程(5 步循环):

① 读取一行到模式空间
② 按顺序执行所有 sed 命令
③ 命令作用于模式空间
④ 输出模式空间内容 → stdout
⑤ 回到步骤①,直到文件末尾

读一行 → 执行全部命令 → 输出 → 读下一行
(对每一行,所有命令串行执行完毕后才输出一次)
1
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

分隔符的灵活使用——当替换内容包含 / 时:

# 路径替换——用 # 或 | 或 _ 作为分隔符
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

反向引用 & 与 \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

# 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

实战:行寻址的常用模式:

# 删除第 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

# 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

# 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

# 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

# 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

# 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

# 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

保持空间速查表:

命令 方向 效果 记忆
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

# 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

多行命令速查:

命令 含义 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

# 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:正则贪婪匹配

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

陷阱 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

陷阱 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

# 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

# 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

# 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

内建变量速查表:

变量 含义 默认值
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

# 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

# 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

# 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

# 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

流程控制速查:

关键词 作用
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

# 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

# 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

# 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:关联数组遍历顺序不确定

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

陷阱 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

陷阱 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

思考题:

  1. sed 反转文件:seq 1 5 | sed -n '1!G; h; $p' 中每一步模式空间和保持空间分别是什么?
  2. awk 分组 TopK:如何用 awk 统计日志中"每个 IP 访问最多的 3 个 URL"?
  3. sed 递归替换:sed ':a; s/\b\([0-9]\+\)0\b/ \1 /; ta' 的作用是什么?
  4. 跨行匹配:如何用 sed 删除 C 语言风格的多行注释 /* ... */?
  5. awk 实现 uniq -c:如何只用 awk 实现 sort | uniq -c | sort -rn 的效果?(提示:关联数组 + END 遍历输出)
#Shell#文本
上次更新: 2026/06/17, 12:47:39
grep 搜索实战
文件查找与统计

← grep 搜索实战 文件查找与统计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式