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

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

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

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 / 会匹配成功吗?为什么?欢迎在评论区讨论。

相关推荐
Lsetea6 天前
OpenSSL s_client退出码为0却证书验证失败:严格校验与Shell管道排查
https·shell·ssl证书·openssl·tls
fox_charon7 天前
Windows 下 CLI 参数的引号陷阱:为什么 --resume 'uuid' 会失败
windows·shell·cmd·cli·引号
吴声子夜歌16 天前
Shell编程实例——编写安全的shell脚本(二)
linux·运维·shell
吴声子夜歌18 天前
Shell编程实例——内务及管理任务(一)
linux·运维·shell
吴声子夜歌18 天前
Shell编程实例——高级脚本编程(一)
linux·运维·网络·shell
吴声子夜歌18 天前
Shell编程实例——bash的配置与自定义(二)
linux·运维·shell
吴声子夜歌19 天前
Shell编程实例——与解析相关的任务(二)
linux·运维·shell
吴声子夜歌19 天前
Shell编程实例——脚本编程的附加特性
linux·运维·shell
云计算练习生19 天前
什么是内核?操作系统内核到底管哪些事
linux·windows·操作系统·内核·shell
吴声子夜歌22 天前
Shell编程实例——bash入门
linux·运维·shell