2.8 Shell 脚本进阶

预计阅读时间:18 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 理解文件描述符与高级重定向机制
  • 熟练运用进程替换、字符串处理和数组操作
  • 掌握 Bash 正则表达式匹配和高级控制结构
  • 理解 eval/exec/source 的本质区别
  • 编写高效、可维护的 Shell 脚本

核心知识

  • 文件描述符(File Descriptor, FD)——内核用于标识打开文件的整数,0=stdin,1=stdout,2=stderr
  • 进程替换(Process Substitution)——<(cmd)>(cmd) 语法将命令输出当作文件使用
  • 参数扩展(Parameter Expansion)——${var#pattern}${var%pattern} 等内建字符串操作
  • 关联数组(Associative Array)——declare -A 创建的键值对数据结构,类似其他语言的字典/Map
  • select 语句——生成交互式菜单的内建结构
  • getopts——解析命令行参数的标准内建命令
  • exec——替换当前 Shell 进程或操作文件描述符
  • shopt——控制 Bash Shell 可选行为的开关

知识关联

原理讲解

📝 为什么推荐 [[ ]] 而不是 [ ]?

[ ] 是一个外部命令(等价于 test),而 [[ ]] 是 Bash 的内建关键字。这个根本区别导致了六个关键差异:① 空变量安全——[ -z $var ] 当 $var 为空时变成 [ -z ],语法错误;[[ -z $var ]] 不会拆词,安全;② 支持 && 和 ||——[[ -f $file && -r $file ]] 直接用逻辑运算符,[ ] 需要 -a-o(可读性差且有歧义);③ 支持 =~ 正则匹配——[[ $email =~ ^[a-z]+@ ]][ ] 无法做到;④ 支持模式匹配——[[ $file == *.txt ]] 用通配符匹配;⑤ 不会进行 word splitting——变量无需加引号也不会被拆分;⑥ 性能更好——内建关键字不需要 fork 外部进程。唯一需要注意的是 [[ ]] 是 Bash/Zsh 特有的,sh(POSIX shell)不支持——如果脚本需要跨 Shell 兼容,仍需使用 [ ]

📝 Shell 文件描述符的设计原理

文件描述符(FD)是内核为每个进程维护的"打开文件表"索引。进程启动时自动获得三个 FD:0(stdin)、1(stdout)、2(stderr)。这个设计源自 Unix 的"一切皆文件"哲学——网络连接、管道、设备都是"文件",统一用 FD 操作。FD 的核心操作有两个:复制N>&M 把 FD M 复制到 N)和重定向到文件N>file 把 FD N 指向文件)。"交换 stdout 和 stderr"的技巧 3>&1 1>&2 2>&3 就是 FD 复制的组合:先把 stdout 备份到 FD 3,再把 stderr 重定向到 stdout(此时 stdout 已指向 stderr),最后把 FD 3(原 stdout)重定向到 stderr。Shell 的所有重定向(>、>>、<、|、<<)都是通过 FD 操作实现的——理解 FD 就理解了重定向的一切。

📝 Shell 脚本 vs Python 运维脚本:什么时候用哪个?

Shell 擅长的场景:① 系统管理命令的简单组合(systemctl restart nginx && curl -s localhost);② 管道数据处理(ps aux | grep nginx | awk '{print $2}');③ 所有 Linux 系统都自带 Bash,无需安装依赖。Python 擅长的场景:① 复杂数据结构(JSON/YAML 解析、API 调用);② 错误处理和异常捕获(try/except vs set -e);③ 需要可读性和可维护性的大型脚本(超过 200 行)。经验法则:10 行以内的"胶水"脚本用 Shell,超过 50 行或涉及复杂逻辑时用 Python。两者常配合使用——Shell 负责调度和流程控制,Python 负责复杂的数据处理逻辑。

📝 为什么 Shell 脚本要设计 trap 信号处理?

Shell 脚本可能在执行过程中被中断(Ctrl+C、终端断开、系统关机)。如果不做清理,临时文件会残留、锁文件不会释放、数据库事务可能不一致。trap 的设计让脚本可以在"正常退出"和"异常退出"时都执行清理逻辑:trap 'rm -f /tmp/lock.$$' EXIT 保证无论脚本如何结束都会删除锁文件。trap 支持的信号中,EXIT(退出时执行)和 ERR(命令失败时执行)不是真正的 Unix 信号,而是 Bash 特有的"伪信号"——它们让你用一个 trap 处理所有退出场景,而不需要分别捕获 SIGTERM、SIGINT、SIGHUP 等。

文件描述符

每个进程启动时自动打开三个文件描述符(File Descriptor, FD):0=stdin、1=stdout、2=stderr。FD 是内核用于管理打开文件的整数句柄,你可以手动打开更多 FD(如 FD 3、4……)来精细控制输入输出。FD 操作的核心在于两个概念:复制N>&M 将 FD M 复制到 N)和重定向到文件N>file)。高级技巧如 "交换 stdout 和 stderr"(3>&1 1>&2 2>&3)就是用 FD 复制实现的——先把 stdout 备份到 FD 3,再依次交换 1 和 2。

进程替换

管道(|)的局限在于它只能连接一条命令的 stdout 到另一条的 stdin。当需要将一个命令的输出像文件一样传递给另一个命令时——比如 diff 需要两个文件参数——就需要进程替换。语法 <(cmd) 将 cmd 的 stdout 暴露为一个"伪文件"路径(如 /dev/fd/63),让接收方像读取文件一样读取命令输出。

字符串处理与数组

Bash 提供纯内建的字符串操作(参数扩展),无需启动外部命令即可完成截取、替换、删除等操作。关键模式:#% 分别从开头和末尾删除匹配,单字符为最短匹配,双字符为最长匹配。关联数组(declare -A)是 Bash 4.0 引入的键值对数据结构,让脚本可以处理映射关系,是走向复杂脚本的关键一步。

eval / exec / source 的区别

三者容易混淆:source(或 .)在当前 Shell 进程中逐行执行脚本,环境变量可以传递;exec 用新程序替换当前 Shell 进程(PID 不变),用于切换 Shell 或重定向 FD;eval 对字符串做两次扫描和执行——先解析字符串中的变量和转义,再执行结果。eval 是最危险的:如果字符串中包含用户输入,攻击者可以在第二次扫描时注入任意命令。优先用 ${!var} 间接引用替代 eval 的间接变量场景。

并行执行模型

Shell 的并行不是真正的多线程,而是通过后台进程(&)在子 Shell 中并发执行。wait 等待所有后台进程结束,wait -n(Bash 4.3+)等待任意一个结束——配合计数循环可实现并发数限制。"信号量模式"用变量跟踪运行中的后台任务数,超过上限时调用 wait -n 等一个完成后再启动下一个。

函数与作用域

Bash 函数有两种声明形式:func() { ... }function func { ... }。函数体内部的变量默认是全局的——函数退出后变量依然存在。用 local 声明局部变量是隔离作用域的唯一途径。参数通过位置参数 $1 ~ $9 传递(超过 9 个用 ${10}),$# 是参数个数,"$@" 是全部参数。函数没有独立的"返回值"通道:return 只能返回 0~255 的退出码,真正的"值"要么靠 echo + 命令替换捕获,要么靠全局变量。函数的退出码是最后一条命令的退出码——这是最常见的返回值陷阱。

严格模式与调试

set -euo pipefail 是生产脚本的事实标准:-e 让脚本在命令失败时立即退出(不再"带病运行");-u 把引用未定义变量当作致命错误,当场暴露拼写错误;pipefail 让管道返回最右侧失败命令的退出码,而不是只关心最后一条。注意 -e 有个反直觉设计:ifwhile! 等条件上下文中的失败不会触发退出——这正是为条件判断保留的逃生门。trap 提供信号与退出钩子:trap 'cleanup' EXIT 保证清理逻辑无论正常结束还是被 kill 都会执行;trap 'echo 第 $LINENO 行出错' ERR 在每条失败命令处打印行号,是"穷人版"调试器。

示例代码

文件描述符操作

# 打开 FD 3 写入文件,关闭 FD 3
exec 3> output.log
echo "写入日志" >&3
exec 3>&-

# 同时输出到终端和文件
exec 3>&1                     # 备份 stdout
exec > >(tee -a output.log)   # stdout 转向 tee
echo "这句话同时到屏幕和文件"
exec 1>&3 3>&-               # 恢复 stdout

# 交换 stdout 和 stderr
command 3>&1 1>&2 2>&3

进程替换

# 比较两个目录无需临时文件
diff <(ls dir1) <(ls dir2)

# 合并多个输入源
paste <(cut -f1 file.tsv) <(cut -f2 file.tsv)

# 输出到进程(Bash 4.4+)
echo "data" > >(gzip > output.gz)

字符串处理

str="Hello, Linux World"

# 大小写转换
echo "${str^^}"               # HELLO, LINUX WORLD
echo "${str,,}"               # hello, linux world

# 截取与替换
echo "${str:7:5}"             # Linux
echo "${str/World/Universe}"  # 替换第一个
echo "${str//l/L}"            # 替换所有

# 路径解析
path="/home/user/doc.tar.gz"
echo "${path##*/}"            # doc.tar.gz(文件名)
echo "${path%.*}"             # /home/user/doc.tar(去最后一个扩展名)
echo "${path##*.}"            # gz(扩展名)

数组与关联数组

# 索引数组
servers=("web01" "web02" "db01")
echo "${servers[0]}"          # web01
echo "${#servers[@]}"         # 3(长度)

# 关联数组
declare -A config=([host]="localhost" [port]=3306)
echo "${config[host]}"        # localhost
for key in "${!config[@]}"; do
    echo "$key = ${config[$key]}"
done

正则匹配

# Bash =~ 内建正则
if [[ "alice@example.com" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "有效邮箱"
fi

# 捕获组利用
if [[ "2026-06-14" =~ ([0-9]{4})-([0-9]{2})-([0-9]{2}) ]]; then
    echo "年: ${BASH_REMATCH[1]} 月: ${BASH_REMATCH[2]} 日: ${BASH_REMATCH[3]}"
fi

# grep/sed 正则
grep -Po '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' access.log
sed -E 's/([0-9]+)-([0-9]+)-([0-9]+)/\3\/\2\/\1/' <<< "2026-06-14"

高级控制结构

# select 菜单
PS3="请选择操作: "
select action in "启动" "停止" "退出"; do
    case $REPLY in
        1) systemctl start myapp ;;
        2) systemctl stop myapp  ;;
        3) break ;;
    esac
done

# getopts 参数解析
while getopts "hvf:p:" opt; do
    case $opt in
        v) VERBOSE=1 ;;
        f) CONFIG_FILE="$OPTARG" ;;
        p) PORT="$OPTARG" ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))

eval / exec / source

# source:当前 Shell 中执行
source ~/.bashrc

# exec:替换当前进程
exec /bin/zsh                 # 切换到 zsh(PID 不变)

# eval:二次扫描(谨慎!)
eval "$cmd"                   # 先展开变量再执行
# 安全替代:${!var} 间接引用
echo "${!var_name}"
⚠️ eval 安全警告

永远不要对用户输入或网络数据使用 eval!eval 会执行任意代码,是最常见的 Shell 注入漏洞来源。优先使用间接引用 ${!var} 或关联数组替代。

并行控制

# 基本并行:后台 + wait
for file in *.txt; do
    process_file "$file" &
done
wait

# 控制并发数(信号量模式)
MAX_JOBS=4; running=0
for file in *.txt; do
    while (( running >= MAX_JOBS )); do
        wait -n; ((running--))
    done
    process_file "$file" &
    ((running++))
done
wait

shopt 与性能优化

# 启用递归通配
shopt -s globstar
echo **/*.txt

# 扩展通配符删除
shopt -s extglob
rm !(*.txt|*.md)              # 删除非 txt/md 文件

# 性能:避免循环子进程
# 慢
for f in *.txt; do sed -i 's/old/new/g' "$f"; done
# 快
sed -i 's/old/new/g' *.txt

# 性能:减少管道
grep -c "pattern" file.txt    # 快
cat file.txt | grep "pattern" | wc -l  # 慢

数组遍历与切片实战

# 声明与追加
fruits=("apple" "banana")
fruits+=("cherry")            # 追加元素
read -ra words <<< "a b c"    # 从字符串填充数组

# 安全遍历:必须加双引号,否则含空格的元素被拆词
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

# 切片与子串
echo "${fruits[@]:1:2}"       # 从第 1 个取 2 个
echo "${#fruits[@]}"          # 元素个数
unset 'fruits[1]'             # 删除元素(下标要加引号防通配)

# 关联数组实际案例:统计 access.log 各状态码数量
declare -A codes
while read -r line; do
    code=$(echo "$line" | awk '{print $9}')
    ((codes[$code]++))
done < access.log
for code in "${!codes[@]}"; do
    printf "%s: %s\n" "$code" "${codes[$code]}"
done | sort -k2 -rn

函数详解:参数、返回值、局部变量

#!/bin/bash
readonly MAX_RETRY=3

# 返回"值"的标准姿势:echo + 命令替换
get_disk_usage() {
    local mount="$1"
    df -h "$mount" | tail -1 | awk '{print $5}' | tr -d '%'
}
usage=$(get_disk_usage /)
echo "根分区使用率: ${usage}%"

# 返回状态码:0 成功,非 0 失败
check_port() {
    local port="$1"
    ss -tln | grep -q ":$port "
}
if check_port 3306; then
    echo "MySQL 端口在监听"
else
    echo "MySQL 未启动"
fi

# 参数默认值 + 参数个数校验
deploy() {
    local env="${1:-dev}"     # 默认 dev
    (( $# < 1 )) && { echo "用法: deploy <环境>"; return 1; }
    echo "部署到 $env"
}
deploy prod

# 陷阱:函数返回码 = 最后一条命令的退出码
is_alive() {
    ping -c 1 -W 1 "$1" >/dev/null
    [ $? -eq 0 ] && echo "alive" || echo "dead"
}

sed/awk 在脚本中应用

# 从配置文件读取键值对(config.ini: host=db.example.com / port=3306)
host=$(sed -n 's/^host=//p' config.ini)
port=$(sed -n 's/^port=//p' config.ini)
echo "连接 $host:$port"

# awk 按天汇总日志量
awk '{split($4, t, ":"); day=substr($4,2,11); cnt[day]++}
     END {for (d in cnt) print d, cnt[d]}' access.log

# awk 统计平均值 / 最大值(监控脚本常用)
awk '{sum+=$1; if($1>max) max=$1; n++}
     END {print "平均:", sum/n, "最大:", max}' data.txt

# sed 批量修改配置(先 dry-run 确认再写回)
sed -E 's/^(max_connections)\s*=.*/\1 = 500/' my.cnf

# 组合:提取所有 5xx 响应的 URL 并去重统计
awk '$9 ~ /^5/ {print $7}' access.log | sort | uniq -c | sort -rn | head

脚本调试:bash -x 与 trap

# 方式一:执行时开启跟踪(每行先打印展开后的命令)
bash -x myscript.sh

# 方式二:脚本内局部开启/关闭
set -x          # 开启跟踪
# ... 可疑代码段 ...
set +x          # 关闭

# 方式三:定制 PS4,带文件名和行号输出
PS4='+${BASH_SOURCE}:${LINENO}: '
bash -x myscript.sh

# trap 调试:错误时打印行号和出错命令
trap 'echo "第 $LINENO 行出错: $BASH_COMMAND (退出码 $?)"' ERR

# 限制递归深度,防死循环把系统拖垮
export FUNCNEST=50

# 查看函数定义位置
type -a myfunc

# 终极武器:bashdb(类似 gdb 的断点调试器)
# sudo apt install bashdb

错误处理模式:check_returncode 与重试

#!/bin/bash
set -euo pipefail

# 统一失败处理:带上下文退出
check_returncode() {
    local last=$?
    local msg="$1"
    if (( last != 0 )); then
        echo "[ERROR] $msg (exit code: $last)" >&2
        exit "$last"
    fi
}
mkdir -p /backup/daily
check_returncode "创建备份目录失败"

# 带退避的重试机制(网络请求场景)
retry() {
    local -i max=$1; shift
    local -i n=0
    while (( n < max )); do
        if "$@"; then
            echo "第 $((n+1)) 次成功"
            return 0
        fi
        ((n++))
        echo "第 $n 次失败,5 秒后重试..."
        sleep 5
    done
    echo "重试 $max 次全部失败" >&2
    return 1
}
retry 3 curl -fsS https://api.example.com/health

# 利用 -e 的逃生门:允许失败的调用必须放在条件里
if ! rm -f /var/lock/old.pid; then
    :   # 文件不存在不算错误
fi

并发执行:xargs -P 与 GNU parallel

# xargs -P:最简单的高并发工具
# 并行压缩,同时最多 4 个
ls *.log | xargs -P 4 -I{} gzip {}

# 并行下载 100 个 URL(最多 10 并发,失败重试 2 次)
cat urls.txt | xargs -P 10 -n1 wget -q -t 2

# 并行统计每个日志文件的错误数
find /var/log -name "*.log" | xargs -P 8 -I{} \
    sh -c 'echo "{}: $(grep -c ERROR "{}" 2>/dev/null || echo 0)"'

# GNU parallel:比 xargs 更强大的任务分发器(sudo apt install parallel)
# 按 CPU 核数分派,超出自动排队
seq 1 100 | parallel -j $(nproc) "curl -s -o /dev/null https://example.com/api/{}"

# 任一失败立即中止并返回非零
seq 1 10 | parallel --halt now,fail=1 "command {}"

# 每个任务限定超时
cat urls.txt | parallel --timeout 30 "curl -sI {} | head -1"

脚本安全:变量引用与临时文件陷阱

#!/bin/bash
set -euo pipefail

# 1. 始终加引号:未加引号会触发单词分割 + 通配展开
# 危险:路径含空格会拆成多个参数,含 * 会被展开
# rm $file
# 安全:
rm -- "$file"

# 2. 临时文件必须用 mktemp(固定路径 = 符号链接攻击面)
# 危险:tmp=/tmp/backup.tgz; touch "$tmp"
# 安全:
tmp=$(mktemp /tmp/backup.XXXXXX.tgz)
trap 'rm -f "$tmp"' EXIT      # 退出即清理,不残留
mysqldump -A | gzip > "$tmp"

# 3. set -u 陷阱:未定义变量直接报错退出
echo "${USER:-unknown}"       # 有默认值才安全

# 4. 目录安全:cd 失败必须退出,避免在错误目录执行 rm -rf
cd /safe/path || exit 1
rm -rf ./*                    # 相对通配,防止误删

# 5. 权限检查:脚本被他人可写 = 提权漏洞
perm=$(stat -c %a "$0")
case "$perm" in
    *2*|*7*) echo "警告: 脚本权限过宽 ($perm)" ;;
esac

常见错误

错误表现根因正确做法
脚本在数组遍历时结果异常未使用 "${array[@]}" 双引号,导致包含空格的元素被拆分为多个单词始终使用双引号:for item in "${arr[@]}"
eval 执行了用户输入中的命令对不可信输入使用了 eval,导致命令注入漏洞优先用 ${!var} 间接引用或关联数组替代 eval
read 读取文件时跳过了最后一行while read line 在文件没有尾随换行符时会返回 false使用 while read -r line || [[ -n $line ]]
使用 =~ 正则匹配时不生效正则表达式未引号包裹时,某些特殊字符会被 Shell 解释将正则放在变量中:re='^[0-9]+$'; [[ $var =~ $re ]]
变量赋值 var=value 两边有空格Shell 将 var = value 解析为命令而非赋值赋值时等号两侧绝对不能有空格:var=value

最佳实践

实践原理示例
脚本开头设置 set -euo pipefail错误时立即退出、未定义变量报错、管道中间失败也检测#!/bin/bash; set -euo pipefail
始终对变量引用加双引号防止单词分割和路径名展开(globbing)cp "$src" "$dst" 而非 cp $src $dst
[[ ]] 替代 [ ][[ ]] 是 Bash 关键字,支持正则匹配和模式匹配,无单词分割问题[[ $str == *.txt ]]
使用 local 声明函数内变量防止函数变量污染全局作用域myfunc() { local tmpfile; tmpfile=$(mktemp); ... }
有意义且一致的命名增强代码可读性和可维护性readonly MAX_RETRIES=3 而非 m=3
readonly 声明常量防止常量被意外修改readonly CONFIG_FILE="/etc/myapp.conf"

练习题

  1. (概念)文件描述符 0、1、2 分别对应什么?如何将 stdout 和 stderr 同时重定向到同一个文件?
  2. (概念)${var##*/}${var%.*} 分别做什么?它们的匹配策略(最长/最短)有什么区别?
  3. (实操)编写一个脚本,使用 declare -A 定义至少 3 个用户的登录信息(用户名、IP、端口),然后用 select 菜单让用户选择要 SSH 连接的服务器,执行 ssh 连接。
  4. (实操)用 getopts 编写一个脚本,支持 -f(指定文件)、-v(详细模式)、-n(行数)三个参数,其中 -f-n 需要参数值,然后从指定文件中读取前 N 行输出。
  5. (🔍 挑战)编写一个并发控制脚本:在 /tmp/test_data/ 目录下创建 20 个 .txt 文件,模拟"处理每个文件耗时 1-3 秒"的场景。使用后台进程 + wait -n(Bash 4.3+)将并发数控制在 5 个以内。记录总耗时并与串行执行对比。
点击查看答案
  1. (概念)0=stdin(标准输入)、1=stdout(标准输出)、2=stderr(标准错误)。同时重定向到同一个文件:command > file 2>&1command &> file(Bash 4+)。顺序很重要:2>&1 必须在重定向之后。
  2. (概念)${var##*/} 删除最长前缀匹配(贪婪),取纯文件名;${var%.*} 删除最短后缀匹配(非贪婪),取不含扩展名的部分。例如 var="/path/to/file.txt"${var##*/}file.txt${var%.*}/path/to/file
  3. (实操)核心结构:declare -A servers=( [user1]="user1@10.0.0.1:22" ... )select choice in "${!servers[@]}"IFS=':' read -r user host port <<< "${servers[$choice]}"ssh "$user"@"$host" -p "$port"。注意 selectPS3 提示符设置和退出条件处理。
  4. (实操)样板代码:while getopts "f:vn:" opt; do case $opt in f) file=$OPTARG ;; v) verbose=true ;; n) lines=$OPTARG ;; esac; done。然后 [[ -n "$file" ]] && head -n "${lines:-10}" "$file"。注意 OPTARG 的变量名不要写错,以及 getopts 不会重排参数(参数必须在选项后)。
  5. (🔍 挑战)核心思路:用 for 循环提交 20 个任务,每提交一个存入 pids 数组;当数组长度达到 5 时用 wait -n 等待任意一个完成,然后从数组移除已完成 PID。关键命令:sleep $((RANDOM % 3 + 1)) 模拟耗时,wait -n 只在 Bash 4.3+ 可用,旧版本需用 wait + 轮询替代。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释文件描述符(FD)和进程替换的工作原理尝试向他人讲解
命令操作能不查文档使用 exec 操作文件描述符和进程替换语法在终端实际执行
原理掌握能说出 eval、exec、source 三者的本质区别和使用风险画出执行流程对比图
故障排查能独立调试复杂脚本中的作用域和变量展开问题模拟故障并修复
最佳实践能说明为什么应该用 ${!var} 间接引用替代 eval 的间接变量场景对比不同实现方式

本章总结

本章深入讲解了 Shell 脚本的进阶主题:文件描述符操作让重定向不再局限于 >|;进程替换提供了比管道更灵活的进程间通信方式;字符串处理和数组/关联数组让脚本具备了处理复杂数据结构的能力;正则表达式和高级控制结构(select、getopts)让脚本更接近真正的编程语言。理解 eval/exec/source 的本质差异和掌握并行控制技巧,是你从"能写脚本"到"写好脚本"的关键跨越。

速查表

语法/命令用途
exec 3> file打开文件描述符 3 用于写入
diff <(ls a) <(ls b)进程替换比较两个命令输出
${var#pattern} / ${var##pattern}最短/最长前缀删除
${var%pattern} / ${var%%pattern}最短/最长后缀删除
${var/old/new} / ${var//old/new}替换第一个/所有匹配
declare -A map声明关联数组
[[ $str =~ $re ]]Bash 内建正则匹配
select x in list; do ... done交互式菜单
getopts "ab:c" opt命令行参数解析
shopt -s globstar启用 ** 递归通配
wait -n等待任意一个后台任务完成(Bash 4.3+)
set -euo pipefail严格模式

学习路径建议

延伸阅读

💡 学以致用

本章侧重 Shell 脚本的进阶语法和技巧。想看看这些知识如何应用到真实运维场景中?请继续学习 4.19:Shell 实战案例集「Shell 实战案例集」——包含日志切割、批量备份、健康检查、自动部署等 8 个可直接使用的生产级脚本。

常见问题

Bash 关联数组怎么用?
声明:declare -A map,赋值:map[key]=value,读取:${map[key]},获取所有键:${!map[@]},获取所有值:${map[@]}。声明关联数组必须用 declare -A,普通数组不需要。遍历:for k in "${!map[@]}"; do echo "$k => ${map[$k]}"; done。
trap EXIT 和 trap ERR 有什么用?
trap EXIT 在脚本退出时执行清理操作,如删除临时文件:trap "rm -f $tmpfile" EXIT。trap ERR 在命令失败时触发,适合记录错误日志。配合 set -e 使用:trap 'echo "Line $LINENO failed: $BASH_COMMAND" >> error.log' ERR。
进程替换 <() 和 >() 什么场景用?
进程替换允许将命令输出当作文件使用。diff <(cmd1) <(cmd2) 比较两个命令的输出。while read line; do ... done < <(command) 避免子 shell 问题。实际体验:它解决了管道会创建子 shell 导致变量修改丢失的问题。
↑ 回到顶部