第三章: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" 制表符输出
实战技巧:我在分析Nginx访问日志时,经常用awk做实时统计。比如统计每个IP的访问次数: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  # 等待所有后台任务完成
性能优化的核心原则:能用awk/sed一行搞定的,别写for循环;能用bash内置功能的,别调外部命令;能并行的,别串行。我在项目中总结了一个经验:脚本执行时间超过10分钟,就值得花半小时去优化。

好了,这一章的内容就到这里。正则、sed、awk这三个工具,你用得越熟,写脚本就越顺手。调试和优化是进阶的必经之路,别怕踩坑,踩多了自然就记住了。下一章咱们聊聊更高级的话题——如何把脚本变成真正的运维平台。