AWK & SED 生产力教程 / 第 2 章:SED 基础
第 2 章:SED 基础
SED 的本质很简单:读一行,执行命令,输出结果,重复。
2.1 SED 工作原理
工作流程
┌───────────────┐
│ 输入 (stdin) │
└───────┬───────┘
▼
┌───────────────┐
│ 读入模式空间 │ ← 每次读一行
│ (Pattern Space)│
└───────┬───────┘
▼
┌───────────────┐
│ 执行 SED 命令 │ ← 对模式空间中的内容执行操作
└───────┬───────┘
▼
┌───────────────┐
│ 输出模式空间 │ ← 将结果输出到 stdout
└───────┬───────┘
▼
(读入下一行,重复)
📌 关键概念:
- 模式空间(Pattern Space):SED 的工作缓冲区,每次存放一行
- 保持空间(Hold Space):辅助缓冲区,用于跨行操作(第 3 章详解)
- 地址(Address):指定命令作用于哪些行
基本语法
sed [选项] '命令' 文件...
sed [选项] -e '命令1' -e '命令2' 文件...
sed [选项] -f 脚本文件 文件...
常用选项
| 选项 | 说明 | 示例 |
|---|---|---|
-n | 静默模式,不自动输出 | sed -n '5p' file |
-e | 指定多个命令 | sed -e 's/a/b/' -e 's/c/d/' file |
-i | 原地编辑文件 | sed -i 's/old/new/g' file |
-i.bak | 原地编辑并备份 | sed -i.bak 's/old/new/g' file |
-E | 使用扩展正则表达式 | `sed -E ’s/(foo |
-r | 同 -E(GNU 特有) | `sed -r ’s/(foo |
--follow-symlinks | 跟踪符号链接 | sed -i --follow-symlinks 's/old/new/' link |
⚠️ macOS 用户:
-i必须跟一个备份后缀(可以是空字符串):# Linux (GNU sed) sed -i 's/old/new/g' file # macOS (BSD sed) sed -i '' 's/old/new/g' file # 或使用 brew install gnu-sed
2.2 替换命令(substitute)
替换是 SED 最常用的功能,语法:
s/模式/替换字符串/标志
基本替换
# 创建测试文件
cat > config.txt << 'EOF'
server_name = localhost
server_port = 8080
db_host = 127.0.0.1
db_port = 3306
log_level = DEBUG
EOF
# 将第一处 localhost 替换为 example.com
$ sed 's/localhost/example.com/' config.txt
→ server_name = example.com
→ server_port = 8080
→ db_host = 127.0.0.1
→ db_port = 3306
→ log_level = DEBUG
替换标志
| 标志 | 说明 | 示例 |
|---|---|---|
g | 替换所有匹配(全局) | sed 's/old/new/g' file |
数字 | 替换第 N 处匹配 | sed 's/old/new/2' file |
p | 替换成功时打印 | sed -n 's/old/new/p' file |
w 文件 | 替换成功时写入文件 | sed 's/old/new/w out.txt' file |
i | 忽略大小写 | sed 's/old/new/gi' file |
I | 忽略大小写(GNU 特有) | sed 's/old/new/gI' file |
e | 将结果作为命令执行 | sed 's/^/ls /e' file |
# g — 全局替换
$ echo "aaa bbb aaa" | sed 's/aaa/xxx/g'
→ xxx bbb xxx
# 数字 — 替换第 N 处
$ echo "aaa bbb aaa" | sed 's/aaa/xxx/2'
→ aaa bbb xxx
# p — 打印被修改的行
$ sed -n 's/localhost/example.com/p' config.txt
→ server_name = example.com
# i/I — 忽略大小写
$ echo "Hello HELLO hello" | sed 's/hello/hi/gI'
→ hi hi hi
分隔符
SED 默认使用 / 作为分隔符,但可以用任何字符替代:
# 处理路径时,用 # 避免转义斜杠
$ sed 's#/usr/local/bin#/opt/bin#g' paths.txt
# 处理 URL 时,用 | 作分隔符
$ sed 's|http://|https://|g' urls.txt
# 用逗号作分隔符
$ echo "one,two,three" | sed 's/,/|/g'
→ one|two|three
💡 选择分隔符的原则:选择在模式和替换字符串中不会出现的字符。
反向引用
在替换字符串中引用正则分组:
# \1, \2 ... 引用捕获组
$ echo "John Smith" | sed -E 's/([A-Za-z]+) ([A-Za-z]+)/\2, \1/'
→ Smith, John
# 交换两个单词
$ echo "foo bar" | sed -E 's/(\w+) (\w+)/\2 \1/'
→ bar foo
# 添加引号
$ echo "name=value" | sed -E 's/=(.+)/="\1"/'
→ name="value"
# \0 或 & 代表整个匹配
$ echo "error: something" | sed 's/.*/[&]/'
→ [error: something]
📌 记住:
\0和&都代表整个匹配串,不是第 0 个分组。
2.3 删除命令(delete)
# 删除匹配的行
sed '/模式/d' 文件
# 删除第 N 行
sed 'Nd' 文件
# 删除范围
sed 'M,Nd' 文件
示例
cat > sample.txt << 'EOF'
# 配置文件
# 作者: admin
server_port = 8080
# 已弃用
db_host = localhost
max_connections = 100
EOF
# 删除注释行
$ sed '/^#/d' sample.txt
→
→ server_port = 8080
→ db_host = localhost
→ max_connections = 100
# 删除空行
$ sed '/^$/d' sample.txt
→ # 配置文件
→ # 作者: admin
→ server_port = 8080
→ # 已弃用
→ db_host = localhost
→ max_connections = 100
# 删除注释和空行
$ sed '/^#/d; /^$/d' sample.txt
→ server_port = 8080
→ db_host = localhost
→ max_connections = 100
# 删除第 3 行到第 5 行
$ sed '3,5d' sample.txt
# 删除最后一行
$ sed '$d' sample.txt
⚠️ 注意:d 命令会删除整行,不会输出被删除的行。如果要查看被删除的行,使用 w 写入另一个文件。
2.4 插入和追加命令
| 命令 | 说明 | 位置 |
|---|---|---|
i\文本 | 在匹配行前插入 | insert |
a\文本 | 在匹配行后追加 | append |
c\文本 | 替换匹配行 | change |
cat > hosts.txt << 'EOF'
127.0.0.1 localhost
192.168.1.1 gateway
EOF
# 在第 1 行前插入
$ sed '1i\# Auto-generated by setup.sh' hosts.txt
→ # Auto-generated by setup.sh
→ 127.0.0.1 localhost
→ 192.168.1.1 gateway
# 在 localhost 行后追加
$ sed '/localhost/a\::1 localhost' hosts.txt
→ 127.0.0.1 localhost
→ ::1 localhost
→ 192.168.1.1 gateway
# 替换匹配行
$ sed '/gateway/c\192.168.1.254 router' hosts.txt
→ 127.0.0.1 localhost
→ 192.168.1.254 router
# 插入多行(使用 \n)
$ sed '1i\# Header Line 1\n# Header Line 2' hosts.txt
→ # Header Line 1
→ # Header Line 2
→ 127.0.0.1 localhost
→ 192.168.1.1 gateway
💡 GNU sed 可以省略
\后的换行,写成:sed '1i # Line 1' file
2.5 打印命令
# 打印特定行
$ sed -n '5p' file # 第 5 行
$ sed -n '3,7p' file # 第 3-7 行
$ sed -n '$p' file # 最后一行
# 打印匹配行(类似 grep)
$ sed -n '/error/p' logfile
# 打印行号
$ sed -n '/error/=' logfile
# 同时打印行号和内容
$ sed -n '/error/{=;p}' logfile
# 实用示例:查看配置文件(去掉注释和空行)并显示行号
$ cat -n /etc/ssh/sshd_config | sed '/^$/d' | sed '/^[[:space:]]*#/d'
2.6 写入命令
# 将匹配行写入另一个文件
$ sed -n '/error/w errors.txt' logfile
# 将不同模式的行写入不同文件
$ sed -n '/error/w errors.txt; /warn/w warnings.txt' logfile
# 将范围内的行写入文件
$ sed -n '10,20w subset.txt' logfile
2.7 原地编辑
基本用法
# 直接修改文件(不推荐,无法恢复)
sed -i 's/old/new/g' file
# 修改并备份(推荐)
sed -i.bak 's/old/new/g' file
# file — 修改后的文件
# file.bak — 原始文件
# 不创建备份(确信不需要时)
sed -i '' 's/old/new/g' file # macOS
批量修改多个文件
# 修改当前目录所有 .conf 文件
find . -name "*.conf" -exec sed -i.bak 's/old_domain/new_domain/g' {} +
# 或使用 xargs
find . -name "*.conf" | xargs sed -i.bak 's/old_domain/new_domain/g'
# 使用通配符(如果文件不多)
sed -i.bak 's/old/new/g' *.conf
⚠️ 重要提醒:
- 原地编辑前先不加
-i测试输出是否正确- 首次操作务必带备份后缀
- 修改大量文件前先在少量文件上测试
# ❌ 危险操作(直接修改所有文件)
find . -name "*.py" -exec sed -i 's/old/new/g' {} +
# ✅ 安全操作(先测试,再执行)
# 第一步:预览
find . -name "*.py" | head -5 | xargs sed 's/old/new/g'
# 第二步:确认后执行
find . -name "*.py" -exec sed -i.bak 's/old/new/g' {} +
# 第三步:验证
grep -r "old" *.py
# 第四步:删除备份
find . -name "*.bak" -delete
2.8 多命令执行
方法一:分号分隔
$ sed 's/foo/bar/g; s/baz/qux/g' file
方法二:多个 -e 选项
$ sed -e 's/foo/bar/g' -e 's/baz/qux/g' file
方法三:大括号组合
# 对匹配行执行多个命令
$ sed '/error/{s/error/ERROR/; s/$/ [RETRY]/}' logfile
方法四:脚本文件
# 创建 sed 脚本
cat > transform.sed << 'EOF'
# 删除空行
/^$/d
# 删除注释
/^#/d
# 替换配置值
s/debug_level = [0-9]/debug_level = 1/
# 添加时间戳
s/^/[2024-01-01] /
EOF
# 使用脚本文件
$ sed -f transform.sed config.txt
2.9 实战案例
🏢 场景一:批量修改 Nginx 配置
# 查看修改前的配置
$ grep -n "server_name\|listen\|ssl" /etc/nginx/site.conf
# 修改域名
$ sed -i.bak 's/old-domain\.com/new-domain.com/g' /etc/nginx/site.conf
# 添加 SSL 配置(在 listen 80 后面添加)
$ sed -i '/listen 80;/a\ listen 443 ssl;' /etc/nginx/site.conf
# 注释掉某行
$ sed -i 's/^ access_log/# access_log/' /etc/nginx/site.conf
🏢 场景二:清理 CSV 文件
cat > data.csv << 'EOF'
Name,Age,Email
"Alice",30,"[email protected]"
"Bob", 25,"[email protected]"
"Carol", 35,"[email protected]"
EOF
# 去掉字段中的引号
$ sed 's/"//g' data.csv
→ Name,Age,Email
→ Alice,30,[email protected]
→ Bob, 25,[email protected]
→ Carol, 35,[email protected]
# 去掉多余空格
$ sed 's/, /,/g; s/ ,/,/g' data.csv
🏢 场景三:修改 Docker Compose 文件
# 将 image 版本从 1.0 升级到 2.0
$ sed -i 's/image: myapp:1.0/image: myapp:2.0/' docker-compose.yml
# 注释掉某个 service(简化方法,实际建议用 yq)
$ sed -i '/service-name/{N;s/^/#/}' docker-compose.yml
🏢 场景四:日志文件预处理
# 从日志中提取时间戳和消息
# 原始格式: 2024-01-15 10:23:45 [ERROR] Something went wrong
$ sed -n 's/^\([0-9-]* [0-9:]*\) \[\([A-Z]*\)\] \(.*\)/\1 | \3/p' app.log
# 将多行错误合并为单行(高级用法,参见第 3 章)
2.10 常见陷阱
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
忘记 -i | 没有输出,以为命令没生效 | 原地编辑不会输出到终端 |
| 分隔符冲突 | / 在路径中出现 | 换用 #、` |
| 贪婪匹配 | .* 匹配过多 | 用 .*?(ERE)或更精确的模式 |
macOS -i | 忘记空字符串参数 | sed -i '' '...' |
| 行尾换行符 | 每行末尾有 \n | 注意 $ 匹配的是行尾,不包含 \n |
| 大文件内存 | SED 不会一次性加载所有行 | 这是优势,不是问题 |
2.11 命令速查表
# 替换
s/old/new/ # 替换第一处
s/old/new/g # 全局替换
s/old/new/gi # 全局替换,忽略大小写
# 删除
d # 删除行
3d # 删除第 3 行
3,7d # 删除 3-7 行
/pattern/d # 删除匹配行
# 打印
p # 打印行
3p # 打印第 3 行
/pattern/p # 打印匹配行(需 -n)
= # 打印行号
# 插入/追加/替换
i\text # 在行前插入
a\text # 在行后追加
c\text # 替换整行
# 写入
w file # 将模式空间写入文件
扩展阅读
下一章:第 3 章:SED 进阶 — 掌握地址范围、保持空间和高级流控制。