脚本里用 grep 判断某个配置项在不在文件里,检查脚本跑到这一步就停了,日志里没有任何报错。单独在终端重跑那条命令,什么都没输出,看着也正常。
问题问得具体一点:grep 什么都没匹配到的时候,退出码是多少。
#!/bin/bash
sample="evidence/grep-sample.txt"
cat > "$sample" <<'EOF'
alpha 1
beta 2
gamma 3
ALPHA 4
alpha-beta 5
EOF
echo "=== 有匹配 ==="
grep "alpha" "$sample"
echo "exit=$?"
echo ""
echo "=== 无匹配 ==="
grep "delta" "$sample"
echo "exit=$?"
echo ""
echo "=== 无匹配但用 -c 计数 ==="
grep -c "delta" "$sample"
echo "exit=$?"
echo ""
echo "=== 文件不存在 ==="
grep "alpha" "evidence/no-such-file.txt" 2>/dev/null
echo "exit=$?"
实际输出:
=== 有匹配 ===
alpha 1
alpha-beta 5
exit=0
=== 无匹配 ===
exit=1
=== 无匹配但用 -c 计数 ===
0
exit=1
=== 文件不存在 ===
exit=2
三个退出码各有含义:0 是匹配到了,1 是没匹配到,2 是出错了。1 和 2 分开这一点值得记住,因为「没找到」和「没找到是因为文件根本打不开」在脚本里往往需要不同处理,回落成一个码就没法区分。
回到卡住的脚本。set -e 的规则是任何命令返回非 0 就立刻退出,grep 返回的 1 恰好命中了这条规则。无匹配在 grep 看来是正常结果,在 set -e 看来是失败,两边对同一个退出码的理解不一致。
这个冲突在不同写法下的后果差别很大。
#!/bin/bash
sample="evidence/grep-sample.txt"
echo "=== set -e 下无匹配 ==="
(
set -e
echo "进入子脚本,set -e 已开启"
grep "delta" "$sample"
echo "这一行会执行吗"
)
echo "子脚本退出码=$?"
echo ""
echo "=== set -e 下把 grep 用在 if 里 ==="
(
set -e
if grep -q "delta" "$sample"; then
echo "命中"
else
echo "未命中,但脚本没有中断"
fi
echo "后续步骤照常执行"
)
echo "子脚本退出码=$?"
实际输出:
=== set -e 下无匹配 ===
进入子脚本,set -e 已开启
子脚本退出码=1
=== set -e 下把 grep 用在 if 里 ===
未命中,但脚本没有中断
后续步骤照常执行
子脚本退出码=0
第一种写法里,grep 之后那行 echo 没有出现,脚本在 grep 处直接停了。第二种写法放在 if 条件里就没事,因为条件位置上的命令本来就不参与 set -e 判定。
set -e 还有另一个容易踩的点:管道只在最后一个命令上取退出码。
#!/bin/bash
sample="evidence/grep-sample.txt"
echo "=== 管道下游的退出码掩盖 ==="
echo -n "grep 无匹配接 wc -l,管道整体退出码:"
grep "delta" "$sample" | wc -l > /dev/null
echo "$?"
echo -n "用 PIPESTATUS 取第一个命令的真实退出码:"
grep "delta" "$sample" | wc -l > /dev/null
echo "${PIPESTATUS[0]}"
实际输出:
=== 管道下游的退出码掩盖 ===
grep 无匹配接 wc -l,管道整体退出码:0
用 PIPESTATUS 取第一个命令的真实退出码:1
wc -l 成功执行并返回 0,把前面 grep 的 1 盖掉了。这种管道在 set -e 下不会中断,但也不会告诉你其实什么都没匹配到,属于更隐蔽的一类。PIPESTATUS 是 bash 提供的数组,保存管道中每个命令各自的退出码,取第 0 个就是 grep 的。
如果只是想知道有没有匹配,用 -q 让它不输出内容,退出码的含义不变:
#!/bin/bash
sample="evidence/grep-sample.txt"
echo "=== 无匹配但用 -q 静默 ==="
grep -q "delta" "$sample"
echo "exit=$?"
实际输出:
=== 无匹配但用 -q 静默 ===
exit=1
顺带记一个同类的坑,大小写。默认区分大小写,加 -i 之后命中行数会变。
#!/bin/bash
sample="evidence/grep-sample.txt"
echo -n "case-sensitive alpha: "
grep -c "alpha" "$sample"
echo -n "case-insensitive -i alpha: "
grep -ic "alpha" "$sample"
实际输出:
case-sensitive alpha: 2
case-insensitive -i alpha: 3
样本里 alpha 命中 2 行,加 -i 之后命中 3 行,多出来的那行是 ALPHA。写成脚本时如果忘了 -i,配置项大小写和预期不一样就会得到无匹配,进而触发前面说的中断,报错信息却是空的。
绕开中断有几种做法,各有代价。把 grep 放进 if 条件里最省事,语义也最清楚,缺点是只能拿到布尔结果,需要行内容时得再跑一次。写 grep ... || true 能保住后续流程,代价是「没匹配」和「文件打不开」这两种情况被一起吞掉,真要排查时线索全没了。第三种是先判文件存在再 grep,多一次判断换来退出码 2 的语义保留,如果脚本对「文件缺失」和「无匹配」需要区别对待,这个代价是值的。
前两种更常见,用哪个取决于脚本后面还要不要区分这两种失败。