AWK & SED 生产力教程 / 第 3 章:SED 进阶
第 3 章:SED 进阶
掌握 SED 的进阶特性,你就拥有了一把精密的文本手术刀。
3.1 地址与地址范围
地址类型
SED 的地址决定了命令作用于哪些行:
| 地址格式 | 说明 | 示例 |
|---|---|---|
无地址 | 所有行 | s/old/new/g |
N | 第 N 行 | 3d |
$ | 最后一行 | $d |
N~M | 从第 N 行开始,每隔 M 行 | 1~2p(奇数行) |
/正则/ | 匹配正则的行 | /^#/d |
\%正则% | 匹配正则(自定义分隔符) | \%/tmp/%d |
N,M | 第 N 到 M 行 | 3,7d |
N,+K | 第 N 行及其后 K 行 | 3,+5d |
N,~M | 第 N 行到 M 的倍数行 | 3,~10d |
/正则1/,/正则2/ | 两个正则之间的行 | /BEGIN/,/END/d |
单地址
# 第 5 行
$ sed -n '5p' file
# 匹配 error 的行
$ sed -n '/error/p' logfile
# 最后一行
$ sed -n '$p' file
# 奇数行
$ seq 10 | sed -n '1~2p'
→ 1
→ 3
→ 5
→ 7
→ 9
# 偶数行
$ seq 10 | sed -n '0~2p'
→ 2
→ 4
→ 6
→ 8
→ 10
地址范围
# 第 3 到第 7 行
$ sed -n '3,7p' file
# 从匹配 start 的行到匹配 end 的行
$ sed -n '/BEGIN/,/END/p' config.txt
# 从第 5 行开始,直到匹配 DONE 的行
$ sed -n '5,/DONE/p' file
# 从匹配 START 的行开始,再往后 10 行
$ sed -n '/START/,+10p' file
⚠️ 地址范围陷阱:当地址范围的结束条件在文件中找不到时,范围会持续到文件末尾:
# 如果文件中没有 END,会从 BEGIN 一直打印到文件末尾
$ sed -n '/BEGIN/,/END/p' file
解决方法:使用 0,/END/ 或在脚本中加判断逻辑。
取反地址
在地址前加 ! 表示取反:
# 删除非注释行(保留注释行)
$ sed '/^#/!d' file
# 不修改第 1 行(从第 2 行开始替换)
$ sed '1!s/old/new/g' file
# 删除第 3 行以外的所有行
$ sed '3!d' file
🏢 业务场景:提取配置块
cat > nginx.conf << 'EOF'
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
}
}
server {
listen 443;
server_name example.com;
ssl on;
location / {
proxy_pass http://backend;
}
}
EOF
# 提取第一个 server 块
$ sed -n '/^server {/,/^}/p' nginx.conf
# 提取包含 443 的 server 块(更精确)
$ sed -n '/server {/,/^}/{ /443/p; /^server/p; /^}/p; }' nginx.conf
3.2 多命令与命令分组
分号分隔
# 简单的多命令
$ sed 's/foo/bar/g; s/baz/qux/g' file
# 注意:分号后面不能有多余的空格(在某些版本中)
大括号分组
# 对地址范围内的行执行多个命令
$ sed '/^server {/,/^}/{
s/listen 80/listen 8080/
s/server_name localhost/server_name example.com/
}' nginx.conf
# 对匹配行执行多个操作
$ sed '/ERROR/{
s/ERROR/[ERROR]/
s/$/ *** ALERT ***/
}' logfile
命令分组中的地址
# 在地址范围内使用嵌套地址
$ sed '/^section/,/^end/{
/comment/d
s/value/VALUE/
}' file
3.3 保持空间(Hold Space)
模式空间 vs 保持空间
┌──────────────────────┐ ┌──────────────────────┐
│ 模式空间 (PS) │ │ 保持空间 (HS) │
│ - 当前正在处理的行 │ │ - 临时存储区 │
│ - 每次循环会刷新 │ │ - 不会自动刷新 │
│ - 命令作用于此 │ │ - 跨行数据交换 │
└──────────────────────┘ └──────────────────────┘
保持空间命令
| 命令 | 说明 | 操作 |
|---|---|---|
h | 复制 PS → HS | 覆盖保持空间 |
H | 追加 PS → HS | 追加到保持空间 |
g | 复制 HS → PS | 覆盖模式空间 |
G | 追加 HS → PS | 追加到模式空间 |
x | 交换 PS ↔ HS | 互换内容 |
示例:反转文件行序(tac)
# tac 命令的 SED 实现
$ seq 5 | sed -n '1!G; h; $p'
→ 5
→ 4
→ 3
→ 2
→ 1
原理分析:
第 1 行 "1":
1!G → 不执行(因为是第 1 行)
h → HS = "1"
PS = "1", HS = "1"
第 2 行 "2":
1!G → G → PS = "2\n1"
h → HS = "2\n1"
PS = "2\n1", HS = "2\n1"
第 3 行 "3":
1!G → G → PS = "3\n2\n1"
h → HS = "3\n2\n1"
PS = "3\n2\n1", HS = "3\n2\n1"
第 5 行(最后一行)"5":
1!G → G → PS = "5\n4\n3\n2\n1"
h → HS = "5\n4\n3\n2\n1"
$p → 打印 PS = "5\n4\n3\n2\n1"
示例:合并相邻行
# 将两行合并为一行(奇数行 + 偶数行)
$ seq 6 | sed 'N; s/\n/ /'
→ 1 2
→ 3 4
→ 5 6
# N 命令:读入下一行追加到模式空间
示例:每三行合并
$ seq 9 | sed 'N; N; s/\n/,/g'
→ 1,2,3
→ 4,5,6
→ 7,8,9
🏢 业务场景:将多行记录合并为单行
# LDAP 或类似格式的数据
cat > ldap.txt << 'EOF'
dn: cn=Alice,ou=Users
cn: Alice
mail: alice@example.com
telephoneNumber: 12345
dn: cn=Bob,ou=Users
cn: Bob
mail: bob@example.com
telephoneNumber: 67890
EOF
# 将每条记录合并为一行
$ sed -n '/^dn:/{
N; N; N
s/\n/ | /g
p
}' ldap.txt
→ dn: cn=Alice,ou=Users | cn: Alice | mail: alice@example.com | telephoneNumber: 12345
→ dn: cn=Bob,ou=Users | cn: Bob | mail: bob@example.com | telephoneNumber: 67890
3.4 分支与流程控制
SED 的分支命令
| 命令 | 说明 | 语法 |
|---|---|---|
b | 无条件跳转 | b label |
t | 上次替换成功时跳转 | t label |
T | 上次替换失败时跳转 | T label(GNU 特有) |
:label | 标签定义 | :label |
基本分支
# 无条件跳转到标签
$ sed ':start
s/foo/bar/
t start' file
# 等效于全局替换 s/foo/bar/g
条件分支(t 命令)
# 反复替换直到没有匹配
cat > nested.txt << 'EOF'
a(((b)))c
EOF
# 去掉所有嵌套的括号
$ sed ':loop
s/()//g
t loop' nested.txt
→ abc
🏢 业务场景:多条件分类处理
cat > requests.log << 'EOF'
GET /index.html 200
POST /api/login 401
GET /dashboard 403
DELETE /api/user 500
GET /health 200
EOF
# 根据状态码添加标签
$ sed -E '
s/ (2[0-9]{2})$/ [OK]/
t done
s/ (4[0-9]{2})$/ [CLIENT_ERROR]/
t done
s/ (5[0-9]{2})$/ [SERVER_ERROR]/
t done
s/$/ [UNKNOWN]/
:done
' requests.log
→ GET /index.html 200 [OK]
→ POST /api/login 401 [CLIENT_ERROR]
→ GET /dashboard 403 [CLIENT_ERROR]
→ DELETE /api/user 500 [SERVER_ERROR]
→ GET /health 200 [OK]
3.5 高级正则表达式
捕获组与反向引用
# 简单的格式转换
$ echo "2024-01-15" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'
→ 15/01/2024
# 交换键值对的键和值
$ echo "name=Alice" | sed -E 's/([^=]+)=(.+)/\2=\1/'
→ Alice=name
# 给数字添加千分位(简化版)
$ echo "1234567" | sed -E 's/([0-9])([0-9]{3})([0-9]{3})$/\1,\2,\3/'
→ 1,234,567
非贪婪匹配
GNU sed 支持非贪婪匹配(需要 -E):
# 贪婪匹配(默认)
$ echo '<a>link</a>' | sed -E 's/<.*>//'
→
# 非贪婪匹配
$ echo '<a>link</a>' | sed -E 's/<[^>]*>//'
→ link
# 或使用非贪婪量词(GNU sed 4.2+)
$ echo '<a>link</a>' | sed -E 's/<.*?>//g'
→ link
⚠️ 注意:非贪婪量词
*?、+?等不是所有 SED 版本都支持。使用[^>]*等方式更安全。
零宽断言(Lookaround)
GNU SED 不原生支持零宽断言。需要用变通方法:
# 需求:在数字前加逗号(但不在行首)
# 不能直接用 lookbehind,需要借助捕获组
$ echo "Price: 1234567 dollars" | sed -E 's/([0-9])([0-9]{3})([^0-9])/\1,\2\3/g'
→ Price: 1,234,567 dollars
多行正则
# N 命令将多行读入模式空间后,可以用正则匹配跨行内容
$ printf "line1\nline2\nline3\nline4\n" | sed 'N; s/\nline2\n/ REPLACED /'
→ line1 REPLACED line3
→ line4
3.6 高级替换技巧
替换命令中的特殊序列
| 序列 | 含义 |
|---|---|
\L | 将后续字符转为小写 |
\U | 将后续字符转为大写 |
\l | 将下一个字符转为小写 |
\u | 将下一个字符转为大写 |
\E | 结束 \L 或 \U 的作用范围 |
# 全部转小写
$ echo "Hello WORLD" | sed 's/.*/\L&/'
→ hello world
# 全部转大写
$ echo "Hello WORLD" | sed 's/.*/\U&/'
→ HELLO WORLD
# 首字母大写
$ echo "hello world" | sed -E 's/\b(\w)/\u\1/g'
→ Hello World
# 将匹配部分转大写,其余保持
$ echo "error: something failed" | sed -E 's/error: (.*)/ERROR: \U\1\E/'
→ ERROR: SOMETHING FAILED
3.7 SED 中的多行处理命令
N、P、D 命令
| 命令 | 说明 |
|---|---|
N | 读入下一行追加到模式空间(用 \n 分隔) |
P | 打印模式空间中第一个 \n 之前的内容 |
D | 删除模式空间中第一个 \n 之前的内容,然后重新开始循环 |
🏢 业务场景:处理续行
# 配置文件中有续行(以 \ 结尾)
cat > config << 'EOF'
server_name = \
example.com
server_port = \
8080
log_level = INFO
EOF
# 将续行合并
$ sed ':join
/\\$/{ N; s/\\\n//; b join
}' config
→ server_name = example.com
→ server_port = 8080
→ log_level = INFO
🏢 业务场景:段落处理
# 将段落(空行分隔的文本块)合并为单行
cat > paragraphs.txt << 'EOF'
This is paragraph
one.
This is paragraph
two.
This is paragraph
three.
EOF
$ sed '
# 如果非空行,追加到保持空间
/^$/!{ H; d; }
# 空行时,输出保持空间内容
/^$/{
x
s/^\n//
s/\n/ /g
p
s/.*//
x
}
' paragraphs.txt | sed '/^$/d'
→ This is paragraph one.
→ This is paragraph two.
→ This is paragraph three.
3.8 SED 脚本编写
脚本文件格式
cat > transform.sed << 'SED_EOF'
#!/usr/bin/sed -f
# 这是注释
# 删除空行
/^$/d
# 处理配置
/^#/{
s/^#[[:space:]]*/[COMMENT] /
}
# 替换值
s/debug_level = [0-9]+/debug_level = 1/
SED_EOF
chmod +x transform.sed
$ ./transform.sed config.txt
调试 SED 脚本
# GNU sed 支持调试模式(4.2.2+)
$ sed --debug 's/old/new/g' file
# 打印调试信息
$ sed -n l file # l 命令显示不可见字符
$ sed -n '5{=;l;p}' file # 显示第 5 行的行号、内容和可打印表示
调试策略
# 方法 1:逐步添加命令
$ sed 's/step1/replacement1/' file # 只有第一个命令
$ sed 's/step1/replacement1/; s/step2/replacement2/' file # 添加第二个
# 方法 2:用 w 命令查看中间结果
$ sed 's/a/b/; w /tmp/debug.txt; s/c/d/' file
$ cat /tmp/debug.txt
# 方法 3:限制范围测试
$ sed '1,5s/old/new/g' file # 只处理前 5 行
3.9 综合实战
🏢 场景一:转换 Apache 配置为 Nginx
cat > apache.conf << 'EOF'
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
EOF
$ sed -E '
# 去掉 XML 标签
s/<\/?VirtualHost[^>]*>//g
s/<\/?Directory[^>]*>//g
# 转换指令
s/^\s*ServerName (.+)/ server_name \1;/
s/^\s*DocumentRoot (.+)/ root \1;/
s/^\s*ErrorLog (.+)/ error_log \1;/
s/^\s*CustomLog (\S+) (.+)/ access_log \1 \2;/
# 转换 Directory 块
s/^\s*AllowOverride All/ try_files $uri $uri\/ =404;/
s/^\s*Require all granted//g
# 添加 Nginx 头尾
1i\server {
$a\}
' apache.conf
🏢 场景二:批量修改文件编码声明
# 将 HTML 文件中的 charset 从 ISO-8859-1 改为 UTF-8
find . -name "*.html" -exec sed -i.bak \
's/charset=ISO-8859-1/charset=UTF-8/gI' {} +
🏢 场景三:从 SQL 文件中提取表结构
cat > schema.sql << 'EOF'
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255)
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2)
);
EOF
# 提取表名
$ sed -n 's/CREATE TABLE \([a-z_]*\).*/\1/p' schema.sql
→ users
→ orders
# 提取表名和字段(简化)
$ sed -n '/CREATE TABLE/{
s/CREATE TABLE \([a-z_]*\).*/Table: \1/
p
}
/^\s\+[a-z]/{
s/^\s\+\([a-z_]*\)\s.*/ - \1/
p
}' schema.sql
→ Table: users
→ - id
→ - name
→ - email
→ Table: orders
→ - id
→ - user_id
→ - amount
3.10 SED 进阶速查
# 高级替换
s/(group1)(group2)/\2\1/ # 交换捕获组
s/.*/\L&/ # 转小写
s/.*/\U&/ # 转大写
# 保持空间操作
h # PS → HS (复制)
H # PS → HS (追加)
g # HS → PS (复制)
G # HS → PS (追加)
x # PS ↔ HS (交换)
# 多行命令
N # 读入下一行到 PS
P # 打印 PS 中 \n 前的内容
D # 删除 PS 中 \n 前的内容
# 分支
b label # 无条件跳转
t label # 替换成功时跳转
T label # 替换失败时跳转 (GNU)
:label # 标签定义
扩展阅读
- GNU SED Manual — 多行处理
- SED Flow Control详解
- 《SED & AWK》第 5 章 — Advanced SED
下一章:第 4 章:AWK 基础 — 掌握数据驱动的文本处理语言。