编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
    • 流程控制与函数
    • 数据与 IO 处理
    • grep 搜索实战
    • sed 与 awk 编程
    • 文件查找与统计
    • 日志监控与告警
    • 备份进程与磁盘
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
      • 12.1 安全加固
        • 12.1.1 命令注入防护——引号与参数化
        • 12.1.2 路径注入与 PATH 安全
        • 12.1.3 临时文件安全——mktemp 才是正解
        • 12.1.4 敏感信息保护——密码/密钥/Token
        • 12.1.5 权限最小化——umask / chmod / 降权
        • 12.1.6 输入校验——不可信原则
        • 12.1.7 Shell 安全最佳实践清单
      • 12.2 兼容处理
        • 12.2.1 bash vs sh——你用的是什么 Shell
        • 12.2.2 macOS vs Linux——BSD vs GNU 差异大全
        • 12.2.3 POSIX 兼容写法——跨平台脚本
        • 12.2.4 版本检测与条件加载
        • 12.2.5 大小写敏感与换行符陷阱
      • 12.3 思考题
    • 性能与打包分发
  • 工具脚本

  • ScriptHub
  • Shell-Bash
杨充
2025-08-04
目录

安全与兼容处理

# 第 12 章 安全与兼容处理

# 目录介绍

  • 12.1 安全加固
    • 12.1.1 命令注入防护——引号与参数化
    • 12.1.2 路径注入与 PATH 安全
    • 12.1.3 临时文件安全——mktemp 才是正解
    • 12.1.4 敏感信息保护——密码/密钥/Token
    • 12.1.5 权限最小化——umask / chmod / 降权
    • 12.1.6 输入校验——不可信原则
    • 12.1.7 Shell 安全最佳实践清单
  • 12.2 兼容处理
    • 12.2.1 bash vs sh——你用的是什么 Shell
    • 12.2.2 macOS vs Linux——BSD vs GNU 差异大全
    • 12.2.3 POSIX 兼容写法——跨平台脚本
    • 12.2.4 版本检测与条件加载
    • 12.2.5 大小写敏感与换行符陷阱
  • 12.3 思考题

# 12.1 安全加固

# 12.1.1 命令注入防护——引号与参数化

命令注入是 Shell 脚本最常见的漏洞——当用户输入被拼接到命令字符串中,它可能执行任意代码:

#!/bin/bash

# ===== 攻击演示 =====
# 用户输入:alice; rm -rf /
read -p "输入用户名: " username
# eval "echo Hello $username"        # ❌ 危险!用户输入 "alice; rm -rf /" 会执行 rm

# ===== 防护原则:永远不要拼接命令字符串 =====

# ❌ 危险:变量直接拼接到命令中
grep "$pattern" file.txt
ssh "$host" "ls $path"

# ✅ 安全:参数保持独立,不加额外引号嵌套
grep "$1" file.txt                  # $1 作为 grep 的一个参数,安全
ssh "$host" ls "$path"              # 参数独立传递

# ===== Shell 特殊字符的威力 =====
# 以下字符在命令拼接中都很危险:
# ; | & $ ` ( ) < > " ' # \ ! ~ { } [ ]

# ===== 防护措施 1:严格过滤输入 =====
sanitize_input() {
    local input="$1"
    # 移除危险字符(具体策略取决于业务)
    echo "$input" | tr -d ';&|`$(){}[]<>!#~'
}

# ===== 防护措施 2:用 -- 停止选项解析 =====
# ❌ 用户输入 -rf 会被当作选项
rm "$user_input"

# ✅ -- 之后的内容不再被解析为选项
rm -- "$user_input"
grep -- "$pattern" file.txt

# ===== 防护措施 3:白名单验证 =====
validate_username() {
    local name="$1"
    if [[ ! "$name" =~ ^[a-zA-Z_][a-zA-Z0-9_-]{2,31}$ ]]; then
        echo "无效的用户名: $name" >&2
        return 1
    fi
}

# ===== 防护措施 4:用安全替代 eval =====
# ❌ eval
eval "echo $user_input"             # 任何内容直接作为代码执行

# ✅ 用 printf 代替
printf '%s\n' "$user_input"         # 安全

# ===== 实战:安全执行外部命令 =====
safe_exec() {
    local cmd="$1"
    # 只允许执行白名单中的命令
    case "$cmd" in
        backup|clean|restart)
            "/opt/scripts/${cmd}.sh"
            ;;
        *)
            echo "不允许的命令: $cmd" >&2
            return 1
            ;;
    esac
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 12.1.2 路径注入与 PATH 安全

#!/bin/bash

# ===== PATH 注入演示 =====
# 如果脚本的 PATH 包含 "." 或用户可写的目录:
# 有人在 /tmp 下放了一个名叫 "ls" 的恶意脚本
# 脚本里写 cd /tmp && ls → 运行了假 ls!
# 它会先搜索 PATH 中的 /tmp(如果在那),执行恶意 ls

# ===== 防护:脚本开头显式设置 PATH =====
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# 1. 不包含 "."(当前目录)
# 2. 不包含用户可写的目录
# 3. 使用绝对路径

# ===== 变量引用 IFS 安全 =====
# IFS 控制分词——恶意 IFS 可让 "ls" 变成 "l" 和 "s"
IFS=$' \t\n'                                 # 开头重置为安全值

# ===== 路径验证 =====
validate_path() {
    local path="$1"
    # 检测路径遍历攻击(../)
    if [[ "$path" =~ \.\./ ]]; then
        echo "禁止路径遍历" >&2
        return 1
    fi
    # 限制在允许的目录下
    local real_path=$(realpath "$path" 2>/dev/null || readlink -f "$path")
    if [[ "$real_path" != /opt/data/* ]]; then
        echo "路径超出允许范围" >&2
        return 1
    fi
}

# ===== 安全地查找命令 =====
# ❌ 依赖 PATH
ls /tmp

# ✅ 用完整路径
/bin/ls /tmp
# 或先找到绝对路径:
CMD=$(command -v ls)
"$CMD" /tmp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 12.1.3 临时文件安全——mktemp 才是正解

#!/bin/bash

# ===== mktemp —— 安全创建临时文件/目录 =====
# 自动生成唯一文件名,防止竞态条件(race condition)

# 临时文件
tmpfile=$(mktemp)                              # /tmp/tmp.XXXXXX
tmpfile=$(mktemp /tmp/myscript.XXXXXX)         # 指定前缀和路径

# 临时目录
tmpdir=$(mktemp -d)                            # /tmp/tmp.XXXXXX 目录

# ===== ❌ 不安全的做法 =====
# 可预测的文件名容易被攻击者利用(Symlink 攻击)
tmpfile="/tmp/my_script_$$.tmp"                # $$ = PID——可预测!
# 攻击者可以先创建 /tmp/my_script_12345.tmp 的软链接,指向 /etc/shadow

# ===== 安全模板 =====
safe_temp_usage() {
    local tmpdir=""
    local tmpfile=""

    # 创建临时目录
    tmpdir=$(mktemp -d)
    # 自动清理——无论脚本如何退出
    trap 'rm -rf "$tmpdir"' EXIT

    tmpfile="$tmpdir/data.txt"

    # 安全创建(设置严格权限)
    # mktemp 默认创建 600 权限(只有 owner 可读写)
    install -m 600 /dev/null "$tmpfile"

    # 使用临时文件...
    echo "sensitive data" > "$tmpfile"

    # trap 会在退出时自动删除 $tmpdir
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 12.1.4 敏感信息保护——密码/密钥/Token

#!/bin/bash

# ===== 原则:不在命令行中暴露密码 =====
# ❌ 密码在 ps 输出中可见
mysql -u root -p"my_password" mydb              # ps aux 能看到!

# ✅ 方式 1:配置文件(推荐)
cat > ~/.my.cnf << 'EOF' && chmod 600 ~/.my.cnf
[client]
user=root
password=my_password
EOF
mysql --defaults-file=~/.my.cnf mydb

# ✅ 方式 2:环境变量
export MYSQL_PWD="my_password"
mysql -u root mydb                              # 命令行不写密码
# ⚠️ 注意:环境变量在 /proc/<pid>/environ 中可能被同用户看到

# ✅ 方式 3:从 stdin 管道
echo "my_password" | mysql -u root -p mydb
# 或
mysql -u root -p < <(echo "my_password")

# ✅ 方式 4:从加密文件读取(生产推荐)
# 先用 gpg 加密密码,脚本中解密后使用
PASSWORD=$(gpg --decrypt /secure/db_pass.gpg 2>/dev/null)

# ===== API Token 保护 =====
# ❌ 硬编码
TOKEN="sk-abc123def456"
# ✅ 环境变量
TOKEN="${API_TOKEN:-}"                         # 从环境变量读取
if [[ -z "$TOKEN" ]]; then
    die "请设置环境变量 API_TOKEN"
fi

# ===== 脚本自身不包含密钥 =====
# ✅ 从 Vault / AWS Secrets Manager / HashiCorp Vault 获取
secret=$(vault kv get -field=value secret/myapp/db 2>/dev/null)

# ===== 清理敏感信息 =====
# 脚本退出时从内存中清除
cleanup() {
    unset PASSWORD TOKEN API_KEY               # unset 变量
    # 可选:shred 包含密码的临时文件
    shred -u "$PASSWORD_FILE" 2>/dev/null
}
trap cleanup EXIT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 12.1.5 权限最小化——umask / chmod / 降权

#!/bin/bash

# ===== 设置安全的 umask =====
# umask 决定新建文件的默认权限
# 777 - umask = 实际权限
umask 027                       # 新文件: 750 (owner=rwx group=rx other=---)
# 推荐服务器脚本: umask 027 或 umask 077

# ===== 文件创建后显式设权限 =====
# 原则:能用 600 不用 644
touch /etc/myapp/config.conf
chmod 600 /etc/myapp/config.conf             # 只有 owner 可读写
chown root:root /etc/myapp/config.conf

# ===== 安全检查 =====
check_insecure_permissions() {
    echo "检查不安全权限..."

    # 检查 .ssh 目录和 authorized_keys
    find /home -maxdepth 2 -name ".ssh" -not -perm 700 -exec ls -ld {} \;
    find /home -maxdepth 3 -name "authorized_keys" -not -perm 600 -exec ls -l {} \;

    # 检查其他用户可写的脚本目录
    find /usr/local/bin /opt -type f -perm /o+w
}

# ===== 降权运行:脚本不要全程 root =====
# ❌ 整个脚本 root 运行
setup_web_app() {
    apt install nginx
    mkdir -p /var/www/html
    # ... 全部操作 ..
}

# ✅ 管理员部分 root,应用部分降权
setup_web_app() {
    # ---- root 部分 ----
    apt install nginx
    adduser --system www-data

    # ---- 降权到 www-data 执行 ----
    sudo -u www-data bash << 'APP'
        mkdir -p /var/www/html
        cp /tmp/deploy/* /var/www/html/
    APP
}

# 或需要 root 临时提权:sudo 只授权必要命令
# 在 sudoers 中:
# appuser ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 12.1.6 输入校验——不可信原则

#!/bin/bash

# ===== 所有外部输入都不可信 =====

# IP 地址校验
validate_ip() {
    local ip="$1"
    local pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
    if [[ ! "$ip" =~ $pattern ]]; then
        die "无效的 IP 地址: $ip"
    fi
    # 每个字节还须 <= 255
    IFS='.' read -ra octets <<< "$ip"
    for oct in "${octets[@]}"; do
        if (( oct > 255 )); then
            die "无效的 IP 地址: $ip"
        fi
    done
}

# 端口号校验
validate_port() {
    local port="$1"
    if [[ ! "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
        die "无效的端口号: $port"
    fi
}

# 文件名校验(白名单)
validate_filename() {
    local name="$1"
    # 只允许字母、数字、下划线、连字符、点
    if [[ ! "$name" =~ ^[a-zA-Z0-9._-]+$ ]]; then
        die "无效的文件名: $name"
    fi
}

# ===== 组合校验:多层防御 =====
# 1. 类型检查(数字/字符串)
# 2. 范围检查(长度/大小)
# 3. 格式检查(正则/白名单)
# 4. 业务规则检查

validate_email() {
    local email="$1"
    # 1. 长度
    [[ ${#email} -le 254 ]] || return 1
    # 2. 格式
    [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] || return 1
    # 3. 域名部分
    local domain="${email#*@}"
    host "$domain" > /dev/null 2>&1 || return 1
    return 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 12.1.7 Shell 安全最佳实践清单

#!/bin/bash
# ===== 每条脚本应遵循的安全检查清单 =====

# ☐ 1. 开头设置
set -euo pipefail
IFS=$' \t\n'
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# ☐ 2. 不硬编码密码/密钥
#     使用环境变量 ~/.my.cnf 或 Vault

# ☐ 3. 使用 mktemp 创建临时文件
#     配合 trap 自动清理

# ☐ 4. 所有外部输入加校验
#     包括 $1/$2/用户输入/文件内容/环境变量

# ☐ 5. 变量引用加双引号
#     除非明确需要分词

# ☐ 6. 用 -- 结束选项解析
#     rm -- "$file" / grep -- "$pattern"

# ☐ 7. 限制文件权限
#     umask 027 / chmod 600 关键文件

# ☐ 8. 最小权限原则
#     不需要 root 时降权执行

# ☐ 9. 不执行拼接的命令字符串
#     禁止 eval / 禁止动态拼接 SQL/命令

# ☐ 10. 运行 shellcheck
#     shellcheck script.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 12.2 兼容处理

# 12.2.1 bash vs sh——你用的是什么 Shell

#!/bin/bash

# ===== Shebang 决定用哪个解释器 =====
#!/bin/sh          # POSIX Shell(可能是 dash/ash/bash--posix)
#!/bin/bash        # Bash(本系列教程使用)
#!/usr/bin/env bash  # 从 PATH 中找 bash(更可移植)

# ===== 检查当前 Shell =====
echo "$SHELL"                              # 默认 Shell(用户登录用的)
echo "$BASH_VERSION"                       # bash 版本(sh 下为空)
ps -p $$                                    # 当前进程名

# ===== bash 独有特性——在 sh 下运行会报错 =====
# 1. 数组
arr=("a" "b" "c")                         # sh 不支持
# 2. [[ ]] 条件测试
[[ -f "$file" && -x "$file" ]]            # sh 只能用 [ ]
# 3. 进程替换
diff <(sort a.txt) <(sort b.txt)          # sh 不支持
# 4. here-string
tr 'a-z' 'A-Z' <<< "hello"                # sh 不支持
# 5. {start..end} 范围展开
echo {1..10}                              # sh 需要 $(seq 1 10)
# 6. 函数局部变量
local var="hello"                         # sh 不支持 local

# ===== 检测脚本是否在 bash 下运行 =====
if [[ -z "$BASH_VERSION" ]]; then
    echo "错误: 此脚本需要 bash 运行" >&2
    exit 1
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 12.2.2 macOS vs Linux——BSD vs GNU 差异大全

这是跨平台 Shell 脚本最让人头疼的问题——同一命令在 macOS 和 Linux 上行为不同:

#!/bin/bash

# ===== 差异 1: sed =====
# macOS (BSD sed)
sed -i '' 's/old/new/g' file.txt          # -i 必须带参数(可以是空)

# Linux (GNU sed)
sed -i 's/old/new/g' file.txt             # -i 不需要参数

# ✅ 兼容写法
if [[ "$(uname)" == "Darwin" ]]; then
    sed -i '' 's/old/new/g' file.txt
else
    sed -i 's/old/new/g' file.txt
fi

# ===== 差异 2: 命令选项 =====
# grep -P (Perl 正则)
# macOS:默认不支持 -P → 需要 brew install grep (ggrep)
# Linux:支持 -P

# stat 查看文件信息
# macOS: stat -f "%z %N" file.txt         # -f 指定格式
# Linux: stat -c "%s %n" file.txt         # -c 指定格式

# date 计算日期
# macOS: date -v -1d +%Y%m%d              # -v 调整
# Linux: date -d "yesterday" +%Y%m%d       # -d 解析

# ===== 差异 3: 内置工具路径 =====
# /bin/bash 位置
# macOS: /bin/bash (是 bash 3.x)
# Linux: /bin/bash (通常是 bash 4.x/5.x)

# ✅ 用 env 查找
#!/usr/bin/env bash

# ===== 兼容检测函数 =====
is_macos()  { [[ "$(uname)" == "Darwin" ]]; }
is_linux()  { [[ "$(uname)" == "Linux" ]];  }

# macOS 缺少 GNU 工具时自动安装提示
require_gnu_tool() {
    local tool="$1"
    local brew_pkg="${2:-$tool}"

    if is_macos; then
        if ! command -v "$tool" &>/dev/null; then
            echo "macOS 缺少 $tool,请安装: brew install $brew_pkg"
            return 1
        fi
    fi
}

# ===== 通用跨平台函数 =====
# 获取文件大小
get_file_size() {
    local file="$1"
    if is_macos; then
        stat -f%z "$file"
    else
        stat -c%s "$file"
    fi
}

# 获取昨天日期
get_yesterday() {
    if is_macos; then
        date -v-1d +%Y-%m-%d
    else
        date -d "yesterday" +%Y-%m-%d
    fi
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 12.2.3 POSIX 兼容写法——跨平台脚本

如果脚本需要在 sh/dash/ash/bash 等不同 Shell 上运行,遵循 POSIX 标准:

#!/bin/sh

# ===== POSIX 替代方案对照表 =====
# Bash 写法               POSIX 写法
# [[ ]]                   [ ]
# == (in [[ ]])           =
# function name() {}      name() {}
# local var               (不用 local,或用子 shell)
# &> /dev/null            >/dev/null 2>&1
# source file             . file
# ((i++))                 i=$((i+1))
# ${arr[@]}               (POSIX 无数组,用位置参数)
# {1..10}                 $(seq 1 10)
# <(cmd)                  用临时文件

# ===== 示例:POSIX 兼容的字符串处理 =====
# 拿到文件扩展名
ext=${filename##*.}                         # POSIX 和 Bash 都支持
# 拿到文件名(去扩展名)
base=${filename%.*}                         # POSIX 和 Bash 都支持

# ===== 注意:POSIX 也支持以下特性 =====
# 1. $(command) 命令替换(非反引号)
# 2. ${var:-default} 默认值
# 3. ${var#pattern} 模式匹配
# 4. $((expr)) 算术运算
# 5. case 语句
# 6. here-document (cat << EOF)

# ===== POSIX 兼容的日志函数(不用 local)=====
_log() {
    _level="$1"; shift
    echo "[$(date +%H:%M:%S)] $_level: $*"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 12.2.4 版本检测与条件加载

#!/bin/bash

# ===== 检测 bash 版本 =====
check_bash_version() {
    local required_major="${1:-4}"
    if (( BASH_VERSINFO[0] < required_major )); then
        echo "需要 bash >= ${required_major}.x,当前: $BASH_VERSION" >&2
        exit 1
    fi
}
# check_bash_version 4

# ===== 检测命令是否存在 =====
require_cmd() {
    for cmd in "$@"; do
        if ! command -v "$cmd" &>/dev/null; then
            echo "错误: 缺少命令 $cmd,请安装后重试" >&2
            exit 1
        fi
    done
}
# require_cmd jq curl docker

# ===== 检查命令支持特定选项 =====
check_cmd_option() {
    local cmd="$1" opt="$2"
    case "$cmd" in
        grep)
            grep "$opt" /dev/null 2>/dev/null
            ;;
        sed)
            sed "$opt" /dev/null 2>/dev/null
            ;;
    esac
}

# ===== 按操作系统加载不同实现 =====
if is_macos; then
    OPEN_CMD="open"
    PKG_MANAGER="brew"
elif is_linux; then
    OPEN_CMD="xdg-open"
    if command -v apt &>/dev/null; then
        PKG_MANAGER="apt"
    elif command -v yum &>/dev/null; then
        PKG_MANAGER="yum"
    fi
fi

# 安装依赖的跨平台函数
install_dep() {
    local pkg="$1"
    if [[ "$PKG_MANAGER" == "brew" ]]; then
        brew install "$pkg"
    elif [[ "$PKG_MANAGER" == "apt" ]]; then
        sudo apt-get install -y "$pkg"
    elif [[ "$PKG_MANAGER" == "yum" ]]; then
        sudo yum install -y "$pkg"
    fi
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 12.2.5 大小写敏感与换行符陷阱

#!/bin/bash

# ===== 换行符问题 (CRLF vs LF) =====
# Windows 文件用 CRLF (\r\n),Linux/macOS 用 LF (\n)
# 在 Linux 上运行 CRLF 脚本会报:
# /bin/bash^M: bad interpreter: No such file or directory

# 检测:
file script.sh | grep "CRLF"
cat -A script.sh | grep '\^M'

# 修复:
sed -i 's/\r$//' script.sh               # Linux
# 或:
dos2unix script.sh                         # 需安装

# ===== 文件名大小写 =====
# macOS 文件系统默认大小写不敏感(但保留大小写)
# Linux 文件系统大小写敏感
# ReadMe.md 和 README.md 在 macOS 是同一个文件,Linux 是两个文件

# 安全做法:统一用小写
mv ReadMe.md readme.md
mv MyScript.sh myscript.sh

# ===== 路径分隔符 =====
# 不要硬编码 / 为路径分隔符
# ❌ DIR="/opt/myapp/logs"
# ✅ 考虑 Cygwin/WSL 时用变量
SEP="/"                                    # Unix 统一使用 /

# ===== 检查脚本运行环境 =====
cat > /tmp/env_check.sh << 'SCRIPT'
#!/bin/bash
echo "===== 运行环境诊断 ====="
echo "OS:      $(uname -s)"
echo "Arch:    $(uname -m)"
echo "Shell:   $BASH_VERSION"
echo "Bash路径: $(command -v bash)"
echo "PWD:     $PWD"
echo "PATH:    $PATH"
echo "Home:    $HOME"
SCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 12.3 思考题

  1. 命令注入审计:review 以下代码片段中的安全风险并修复:

    user=$1
    ssh "$user@host" "cd /home/$user && ls -la"
    rm -rf /tmp/$user/*
    
    1
    2
    3
  2. 跨平台备份脚本:写一个兼容 macOS 和 Linux 的目录备份脚本,要求正确使用 stat/sed/date,不依赖任何平台特定选项。

  3. 敏感信息保护:设计一个方案,让 Shell 脚本在执行过程中使用临时 API Token,脚本结束后确保 Token 在内存和磁盘上都不可恢复(提示:trap + unset + shred)。

  4. PATH 安全验证:写一个 check_script_security.sh,检查脚本中的以下安全隐患:

    • 硬编码的密码
    • 使用 eval
    • 变量的命令拼接
    • /tmp 固定路径临时文件

    • 未引号的变量引用
  5. POSIX 改造:将一个使用了 [[ ]]、数组、local、&> 的 bash 脚本改写成 POSIX sh 兼容版本,并验证在 dash 下能正常运行。

#Shell#技巧
上次更新: 2026/06/17, 12:47:39
调试与脚本规范
性能与打包分发

← 调试与脚本规范 性能与打包分发→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式