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 可选行为的开关
知识关联
- 前置知识:1.9:重定向与管道 管道与重定向、1.7:用户与权限管理 文本处理三剑客
- 后续影响:本章内容是 4.4:Shell 自动化实战 Shell 自动化实战和 4.12:Python 运维脚本 Python 运维脚本的基础
- 配套技术:2.10:正则表达式 正则表达式、3.15:Cron 定时任务 Cron 高级定时任务
- 相关章节:3.6:MySQL 数据库 Shell 脚本基础是本章的前置;3.13:备份策略与恢复 数组与字符串处理提供了更多数组操作技巧;5.2:Docker 生产实践 脚本调试与优化深入讲解调试方法
原理讲解
[ ] 是一个外部命令(等价于 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 兼容,仍需使用 [ ]。
文件描述符(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 擅长的场景:① 系统管理命令的简单组合(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 脚本可能在执行过程中被中断(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 有个反直觉设计:if、while、! 等条件上下文中的失败不会触发退出——这正是为条件判断保留的逃生门。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 会执行任意代码,是最常见的 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" |
练习题
- (概念)文件描述符 0、1、2 分别对应什么?如何将 stdout 和 stderr 同时重定向到同一个文件?
- (概念)
${var##*/}和${var%.*}分别做什么?它们的匹配策略(最长/最短)有什么区别? - (实操)编写一个脚本,使用
declare -A定义至少 3 个用户的登录信息(用户名、IP、端口),然后用select菜单让用户选择要 SSH 连接的服务器,执行ssh连接。 - (实操)用
getopts编写一个脚本,支持-f(指定文件)、-v(详细模式)、-n(行数)三个参数,其中-f和-n需要参数值,然后从指定文件中读取前 N 行输出。 - (🔍 挑战)编写一个并发控制脚本:在
/tmp/test_data/目录下创建 20 个.txt文件,模拟"处理每个文件耗时 1-3 秒"的场景。使用后台进程 +wait -n(Bash 4.3+)将并发数控制在 5 个以内。记录总耗时并与串行执行对比。
点击查看答案
- (概念)0=stdin(标准输入)、1=stdout(标准输出)、2=stderr(标准错误)。同时重定向到同一个文件:
command > file 2>&1或command &> file(Bash 4+)。顺序很重要:2>&1必须在重定向之后。 - (概念)
${var##*/}删除最长前缀匹配(贪婪),取纯文件名;${var%.*}删除最短后缀匹配(非贪婪),取不含扩展名的部分。例如var="/path/to/file.txt",${var##*/}得file.txt,${var%.*}得/path/to/file。 - (实操)核心结构:
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"。注意select的PS3提示符设置和退出条件处理。 - (实操)样板代码:
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不会重排参数(参数必须在选项后)。 - (🔍 挑战)核心思路:用
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 | 严格模式 |
学习路径建议
- 学完本章后建议阅读 4.4:Shell 自动化实战 Shell 自动化实战(更多真实案例)
- 正则表达式进阶详见 2.10:正则表达式 正则表达式从入门到精通
- Python 运维脚本可看 4.12:Python 运维脚本 Python 运维脚本
延伸阅读
- Bash 参考手册(官方)
- ShellCheck — Shell 脚本静态分析工具
- Google Shell 风格指南
- 推荐书籍:《高级 Bash 脚本编程指南》(Advanced Bash-Scripting Guide)
本章侧重 Shell 脚本的进阶语法和技巧。想看看这些知识如何应用到真实运维场景中?请继续学习 4.19:Shell 实战案例集「Shell 实战案例集」——包含日志切割、批量备份、健康检查、自动部署等 8 个可直接使用的生产级脚本。