第三章:Shell脚本进阶——正则表达式、sed、awk、脚本调试与性能优化
说实话,很多运维干了两三年,写脚本还停留在「能跑就行」的阶段。我个人觉得,Shell脚本真正进阶的门槛,就是你能不能把正则、sed、awk这三板斧玩明白。今天咱们就聊聊这些硬核内容,顺便分享一些我踩过的坑。
3.1 正则表达式:不只是匹配那么简单
正则表达式这东西,说白了就是一门「模式描述语言」。你告诉它你要找什么,它帮你从一堆文本里捞出来。我见过不少同事,写个IP地址匹配都要折腾半天,其实就是没理解正则的「贪婪」和「懒惰」特性。
3.1.1 基础元字符回顾
| 元字符 | 含义 | 示例 |
|---|---|---|
^ |
行首 | ^root 匹配以root开头的行 |
$ |
行尾 | bash$ 匹配以bash结尾的行 |
. |
任意单个字符 | r.t 匹配rat、rot等 |
* |
前一个字符重复0次或多次 | ab*c 匹配ac、abc、abbc |
[] |
字符集合 | [0-9] 匹配任意数字 |
grep -E 测试,确认匹配对了再往脚本里放。别问我为什么——有一次线上误删日志,就是因为正则写漏了一个转义符。
3.1.2 扩展正则与Perl兼容正则
嗯,这里要注意。Linux里默认的grep用的是基础正则,很多元字符需要转义。比如 + 和 ?,在基础正则里要写成 \+ 和 \?。我个人建议直接用 grep -E 或者 egrep,省心。
# 基础正则:匹配一个或多个数字
grep '[0-9]\{1,\}' file.txt
# 扩展正则:更简洁
grep -E '[0-9]+' file.txt
# Perl兼容正则:功能最强
grep -P '\d+' file.txt
为什么会这样?因为Perl兼容正则支持 \d、\w 这些简写,写起来快多了。我在处理百万行级的日志时,Perl正则的性能优势很明显。
3.2 sed:流编辑器,批量修改的神器
sed这工具,我刚开始觉得它就是个「高级替换」。后来在项目中做配置批量修改,才发现它有多香。你想想看,几百台服务器的配置文件,总不能一台台手动改吧?
3.2.1 基本操作:增删改查
# 替换:把文件中的old替换成new
sed 's/old/new/g' file.txt
# 删除:删除第3到第5行
sed '3,5d' file.txt
# 插入:在第2行后面插入一行
sed '2a\这是新插入的行' file.txt
sed -i 直接修改配置文件,结果正则写错了,把整个文件内容清空了。从那以后,我养成了两个习惯:一是先用 sed 不加 -i 预览结果;二是修改前一定备份,比如 sed -i.bak。
3.2.2 高级用法:多命令与脚本
sed可以一次执行多个命令,用 -e 或者分号隔开。我个人更推荐写sed脚本文件,尤其是复杂的操作。
# 多命令:替换并删除注释行
sed -e 's/foo/bar/g' -e '/^#/d' config.conf
# sed脚本文件
cat > fix.sed << 'EOF'
s/old_version/2.0/g
/^debug/d
s/^port=.*/port=8080/
EOF
sed -f fix.sed config.conf
3.3 awk:文本处理的瑞士军刀
awk是我最常用的文本处理工具,没有之一。它本质上是一种「面向行的编程语言」。你想想看,处理日志、统计报表、格式化输出,awk几乎都能搞定。
3.3.1 核心概念:模式-动作
awk的基本结构就是「模式 { 动作 }」。模式决定哪些行被处理,动作决定怎么处理。我刚开始学的时候,总记不住这个结构,后来发现把它理解成「if条件 then执行」就简单了。
# 打印第2列大于100的行
awk '$2 > 100 { print $1, $3 }' data.txt
# 统计每行字段数
awk '{ print NF, $0 }' file.txt
# 自定义分隔符
awk -F':' '{ print $1, "的家目录是", $6 }' /etc/passwd
3.3.2 内置变量与函数
| 变量 | 含义 | 示例 |
|---|---|---|
NR |
当前行号 | NR==1 处理第一行 |
NF |
当前行字段数 | NF<3 字段少于3的行 |
FS |
字段分隔符 | FS="," 设置逗号分隔 |
OFS |
输出字段分隔符 | OFS="\t" 制表符输出 |
awk '{ ips[$1]++ } END { for(ip in ips) print ip, ips[ip] }' access.log。这个模式-动作-END的结构,是awk的精髓。
3.4 脚本调试:别让Bug过夜
写脚本不出bug是不可能的。我见过最离谱的bug,是一个同事的脚本在测试环境跑了三个月没问题,上线第一天就把生产数据库搞挂了。原因?一个未初始化的变量。
3.4.1 调试模式
# 语法检查(不执行)
bash -n script.sh
# 跟踪执行(显示每条命令)
bash -x script.sh
# 更详细的调试
bash -vx script.sh
我个人习惯在脚本开头加一行 set -x,调试完再注释掉。这样能看到每一步的执行情况,变量值一目了然。
3.4.2 常见陷阱与防范
- 未定义变量:用
set -u让脚本在遇到未定义变量时报错退出 - 管道错误:用
set -o pipefail捕获管道中任何命令的失败 - 命令执行失败:用
set -e让脚本在遇到非零返回值时退出
set -e,结果一个 grep 没匹配到内容就退出了,导致后续清理工作没做。所以 set -e 要慎用,或者配合 || true 使用。
3.5 性能优化:让脚本飞起来
脚本写出来容易,写快难。我记得有一次处理10GB的日志文件,原始脚本跑了6个小时。优化之后,15分钟搞定。差距在哪?说白了就是「减少子进程」和「善用内置功能」。
3.5.1 减少外部命令调用
# 慢:每次循环都调用echo和grep
for file in *.log; do
echo "Processing $file"
grep "ERROR" "$file" >> errors.txt
done
# 快:一次grep搞定
grep "ERROR" *.log > errors.txt
3.5.2 使用内置功能替代外部命令
| 慢的方式 | 快的方式 | 说明 |
|---|---|---|
$(cat file) |
$(<file) |
bash内置读取文件 |
expr $a + $b |
$((a + b)) |
算术扩展,不启动子进程 |
grep -c |
[[ $line =~ pattern ]] |
bash内置正则匹配 |
3.5.3 并行处理
对于可以并行执行的任务,别傻等着。我常用 xargs -P 或者 GNU parallel 来加速。
# 并行压缩日志文件
find logs/ -name "*.log" | xargs -P 4 -I {} gzip {}
# 并行处理多个文件
for file in data_*.csv; do
process_file "$file" &
done
wait # 等待所有后台任务完成
好了,这一章的内容就到这里。正则、sed、awk这三个工具,你用得越熟,写脚本就越顺手。调试和优化是进阶的必经之路,别怕踩坑,踩多了自然就记住了。下一章咱们聊聊更高级的话题——如何把脚本变成真正的运维平台。