Shell 脚本避坑指南:从模式匹配到错误处理的实用技巧

一次危险的命令拦截,背后藏着哪些 Bash 语法细节?

在日常的自动化运维或 AI 辅助编程中,我们经常需要写一些"安全钩子"------比如在执行用户命令之前,检查它是否包含危险模式(rm -rf /mkfsdd 等)。下面的这段代码就是一个典型的预执行检查器:

bash 复制代码
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
    if [[ "$COMMAND" == *"$pattern"* ]]; then
        echo "BLOCKED: Command matches dangerous pattern: $pattern" >&2
        cat <<EOF
{
    "hookSpecificOutput": {
        "permissionDecision": "deny",
        "permissionDecisionReason": "Blocked dangerous pattern: $pattern"
    }
}
EOF
        exit 2
    fi
done

这段脚本只有十几行,却浓缩了 Shell 编程中数组遍历模式匹配多行输出重定向退出码五大核心知识。本文将逐层拆解,并补充日常脚本中极易混淆的细节。


一、数组与循环:"${ARRAY[@]}" 为什么必须加引号?

bash 复制代码
DANGEROUS_PATTERNS=("rm -rf" "mkfs" "dd if=/dev/zero")
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
    ...
done
  • "${DANGEROUS_PATTERNS[@]}":将数组每个元素展开为独立的单词,且保留元素内部的空格(例如 "rm -rf" 不会被拆成 rm-rf)。
  • 如果写成 ${DANGEROUS_PATTERNS[@]}(无引号),数组元素会经历 单词分割路径扩展,包含空格的元素就会被错误拆开。
  • 经验法则 :展开数组时,始终使用双引号 + [@]

二、模式匹配:[[ "$COMMAND" == *"$pattern"* ]] 是正则吗?

不是==[[ ]] 中执行的是 通配符模式匹配(globbing),而非正则表达式。

  • * 表示"任意字符串"(包括空串)。
  • *"$pattern"* 的含义:只要 $COMMAND 包含 $pattern 这个子串就匹配成功。
  • 这与 grep -F(固定字符串)类似,但不支持 .^$ 等正则元字符。

局限 :无法防范变种攻击。比如想禁止 rm -rf /,但用户输入 rm -rf / 时确实会拦截;若用户输入 rm -rf /* 则仍会匹配(因为子串 rm -rf / 存在)。但如果用户写成 rm -rf "${HOME}",模式 rm -rf / 就不能匹配了------这需要更复杂的解析器,简单的子串匹配做不到。


三、输出错误信息:>&2 是什么?

标准输出(文件描述符 1)和标准错误(文件描述符 2)默认都显示在终端,但它们在概念上是分开的流。

  • echo "..." >&2:将原本输出到标准输出的文本 重定向 到标准错误。

  • 为什么这样做?因为脚本的正常结果(例如后面的 JSON)应该输出到 stdout,而错误/诊断信息应该走 stderr,便于调用者区分。例如:

    bash 复制代码
    result=$(./check_command.sh 2>/dev/null)  # 忽略错误输出,只取正确 JSON

>&2 中的 2 是文件描述符编号,不是退出码。它不会终止脚本,只是改变输出流向。


四、多行文本输出:echo vs cat <<EOF

echo 输出多行 JSON(痛苦版)

bash 复制代码
echo "{\n    \"hookSpecificOutput\": {\n        \"permissionDecision\": \"deny\"\n    }\n}"

需要手动转义换行符和双引号,极易出错。

用 here-document(优雅版)

bash 复制代码
cat <<EOF
{
    "hookSpecificOutput": {
        "permissionDecision": "deny"
    }
}
EOF

Here-document 语法 <<EOF ... EOF 的作用:将中间的多行文本直接作为 cat 的标准输入。其中的变量(如 $pattern)会被正常展开。如果想禁止变量展开,使用 <<"EOF"<<'EOF'

何时用 echo,何时用 cat

  • 短字符串、单行echo
  • 多行、含大量引号/花括号/换行cat <<EOF

五、exit 2:数字 2 的含义完全不同

exit 2 表示以退出码 2 终止脚本。退出码 0 代表成功,非 0 代表失败。具体的非零值可由脚本自定义(1 表示一般错误,2 表示参数错误,126 表示命令不可执行等)。

在本文开头的代码中,exit 2 的作用是:

  1. 立即终止脚本,不再检查后续模式。
  2. 告诉父进程"因为危险模式而拒绝执行"(退出码 2 是一种约定)。

对比

语法 作用 是否终止脚本
>&2 重定向标准输出到标准错误
exit 2 退出脚本,返回码 2

二者常配合使用:echo "错误" >&2 输出消息,然后 exit 2 报告失败状态。


六、整合:一个完整的命令拦截器示例

bash 复制代码
#!/bin/bash

# 定义危险模式数组
DANGEROUS_PATTERNS=(
    "rm -rf /"
    "mkfs"
    "dd if=/dev/zero"
    ":(){ :|:& };:"   # fork 炸弹的简单特征
)

# 待检查的命令(通常来自用户输入)
COMMAND="$1"

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
    if [[ "$COMMAND" == *"$pattern"* ]]; then
        # 错误信息送 stderr
        echo "BLOCKED: Command matches dangerous pattern: $pattern" >&2
        # 结构化输出(例如 JSON)送 stdout,供上层工具解析
        cat <<EOF
{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "Blocked dangerous pattern: $pattern. This command could cause irreversible damage."
    }
}
EOF
        exit 2
    fi
done

# 如果没有匹配,放行
echo "Command allowed: $COMMAND"
exit 0

执行效果:

bash 复制代码
$ ./check.sh "sudo rm -rf /home"
BLOCKED: Command matches dangerous pattern: rm -rf /
{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "Blocked dangerous pattern: rm -rf /. This command could cause irreversible damage."
    }
}
$ echo $?
2

七、总结:一张表记住所有疑难点

语法 含义 常见错误
"${ARRAY[@]}" 安全地展开数组所有元素 漏掉引号导致元素被拆分
[[ "$s" == *"$p"* ]] 子串包含匹配(通配符) 误认为支持正则表达式
>&2 重定向标准输出到标准错误 误以为是退出码
cat <<EOF 原样输出多行文本(支持变量展开) 结束符 EOF 前有空格或缩进
exit 2 以退出码 2 终止脚本 >&2 中的 2 混淆

Shell 脚本的这些细节看似琐碎,但每个都是前辈在实践中踩过的坑。掌握它们,你不仅能写出安全的命令拦截器,还能更自信地阅读和修改生产环境中的 Bash 代码。

最后留一个思考题 :如果用户输入的命令是 rm -rf /home/user,上面的模式 rm -rf / 会匹配成功吗?为什么?欢迎在评论区讨论。

相关推荐
柒号华仔15 小时前
「速通Shell」常用命令(中)——进程、网络与系统信息
shell
YuSun_WK20 小时前
shell脚本
shell·脚本
柒号华仔4 天前
「速通Shell」Shell 脚本测试详解
shell
柒号华仔5 天前
「速通Shell」Shell 脚本模块化
shell
柒号华仔6 天前
「速通Shell」Shell编程的错误处理与日志
shell
柒号华仔7 天前
「速通Shell」Shell 数组、关联数组与 mapfile
shell
苏灿烤鱼9 天前
一套进程代替八件套,桌面更稳还是单点更大
linux·github·shell
茶本无香9 天前
通用报表自动化框架:Java调用Shell传参执行PostgreSQL SQL模板
java·sql·postgresql·shell
柒号华仔10 天前
「速通Shell」Shell 循环与遍历
shell·编程语言
十里春风_jzh11 天前
Tabby 修改版:更美观的现代终端
shell·tabby