强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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      # 标签定义

扩展阅读


下一章:第 4 章:AWK 基础 — 掌握数据驱动的文本处理语言。