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

相关推荐
pr_note2 天前
balance_points
shell·tcl
pr_note2 天前
icc2/fc屏蔽指定warning
shell·tcl
诸神缄默不语8 天前
Linux shell脚本教程
linux·bash·shell·sh
liyoro12 天前
用 Codex + 提示词生成一个快速打开 Ghostty 的 macOS 小工具
macos·shell·ai编程
pr_note13 天前
bashrc/alias
shell·tcl
怒放吧德德13 天前
JDK 版本一键切换工具(windows)
后端·shell
vortex515 天前
进程管理器大横评:从 PM2 到 Systemd 的选型与实战
linux·shell·进程管理
Irene199117 天前
Shell 相关基础入门,在 Ubuntu 与 CentOS Shell 中的语法差异总结(bash、dash、sh)
shell
小肝一下17 天前
5. 基础IO
android·linux·shell·基础io·操作系统底层·伊涅夫·伊雷娜