高级 Bash 脚本:掌握用于自动化的 Shell 功能
学习使用数组、进程替换、严格模式、ShellCheck和参数扩展等高级Bash脚本技巧,实现更安全的自动化。
高级Bash脚本:掌握Shell特性实现自动化
当你的脚本从几条命令扩展到真正的自动化时,Bash脚本编写会变得更加困难。你需要更安全的变量处理、更清晰的输入输出,以及更少的临时文件。
本指南涵盖了可在部署脚本、日志检查和维护任务中使用的高级Bash脚本特性。目标不是写出巧妙的Shell代码,而是编写可重复运行、可调试、可交给其他工程师的代码。
1. 使用Bash数组处理真实列表
数组允许你存储多个值,而无需按空格分割字符串。当文件名、服务名或用户输入包含空格时,这一点至关重要。
索引数组
索引数组是最常见的类型,元素通过从0开始的数字索引访问。
示例:
# 初始化一个索引数组
COLORS=("red" "green" "blue" "yellow")
# 访问元素
echo "第二个颜色是:${COLORS[1]}"
# 添加元素
COLORS+=( "purple" )
# 打印所有元素
echo "所有颜色:${COLORS[@]}"
常见数组操作:
| 操作 | 语法 | 描述 |
|---|---|---|
| 获取元素数量 | ${#ARRAY[@]} |
返回数组中元素的总数。 |
| 获取特定元素长度 | ${#ARRAY[index]} |
返回指定索引处字符串的长度。 |
| 遍历 | for item in "${ARRAY[@]}" |
处理所有元素的标准循环结构。 |
始终使用 "${ARRAY[@]}" 引用数组展开。这能确保 "/var/log/my app.log" 作为一个参数而非两个参数传递。
关联数组
关联数组类似于小型键值映射。它们需要Bash 4或更高版本,因此如果你支持较旧的macOS主机,请检查目标系统。
必须使用 -A 声明关联数组:
# 声明为关联数组
declare -A CONFIG_MAP
# 分配键值对
CONFIG_MAP["port"]=8080
CONFIG_MAP["hostname"]="localhost"
CONFIG_MAP["timeout"]=30
# 访问值
echo "端口设置为:${CONFIG_MAP["port"]}"
# 遍历键
for key in "${!CONFIG_MAP[@]}"; do
echo "键:$key, 值:${CONFIG_MAP[$key]}"
done
2. 使用进程替换替代临时文件
进程替换,写作 <(command) 或 >(command),允许命令将另一个命令的输出视为文件。当工具期望文件路径但你的数据来自命令时,这非常有用。
何时使用
例如,假设你需要比较两个生成的服务列表。diff 期望文件路径,但你无需将这些列表写入 /tmp。
不使用进程替换:
# 低效且混乱
output1=$(command_a)
echo "$output1" > /tmp/temp1.txt
output2=$(command_b)
echo "$output2" > /tmp/temp2.txt
diff /tmp/temp1.txt /tmp/temp2.txt
rm /tmp/temp1.txt /tmp/temp2.txt
更简洁的直接比较
进程替换为 diff 提供临时文件描述符,使脚本更简洁。
使用进程替换:
# 简洁的一行比较
diff <(command_a) <(command_b)
此模式适用于 comm、diff 以及需要多个文件输入的工具。
语法变体:
<(command)将命令输出传递给读取器。>(command)将写入的输出发送到另一个命令。
3. 添加严格模式和ShellCheck
高级Bash脚本应在发生意外时大声失败。严格模式有助于在静默损坏发生前捕获缺失变量和损坏的管道。
基本严格模式选项
大多数自动化脚本应以以下内容开头:
set -euo pipefail
-e: 命令失败时退出。-u: 将未设置的变量视为错误。-o pipefail: 如果管道中的任何命令失败,则管道失败。
示例:
# 没有pipefail时,这可能看起来成功,因为wc正常退出
cat file.log | grep successful_pattern | wc -l
# 使用set -o pipefail时,grep失败会使管道失败。
使用ShellCheck
ShellCheck能捕获引用错误、不安全的展开、不可达代码以及常见的可移植性问题。
在提交脚本前运行它:
shellcheck your_script.sh
当ShellCheck要求你引用变量或使用 "${array[@]}" 时,除非有明确理由忽略,否则应将其视为真正的错误。
4. 谨慎捕获输出
使用 $() 进行命令替换很有用,但如果随意使用,它可能隐藏失败或混合输出流。
同时捕获STDOUT和STDERR
当你希望记录命令的所有输出时,同时捕获标准输出和标准错误:
# 将stdout和stderr都捕获到VARIABLE中
VARIABLE=$(command_that_might_fail 2>&1)
# 当只需要退出码时,丢弃stdout和stderr
command_that_might_fail &> /dev/null
使用参数扩展进行内联清理
参数扩展可以在简单情况下清理字符串,而无需启动 sed 或 awk。
${variable%pattern}移除最短的匹配后缀。${variable%%pattern}移除最长的匹配后缀。${variable#pattern}移除最短的匹配前缀。${variable##pattern}移除最长的匹配前缀。
示例:
FILE="report.log.bak"
# 移除匹配.bak的最短后缀
CLEAN_NAME=${FILE%.bak}
echo $CLEAN_NAME # 输出:report.log
# 移除所有匹配*.bak的后缀(这里只移除.bak)
CLEAN_NAME_LONG=${FILE%%.*}
echo $CLEAN_NAME_LONG # 输出:report
何时寻求帮助
当你的脚本删除文件、更改生产服务、处理机密或从CI运行时,请让更有经验的Shell用户审查你的脚本。Bash功能强大,但一个小小的引用错误可能影响匹配模式的所有文件。
要点
使用数组处理真实列表,使用进程替换处理类似文件的命令输出,使用 set -euo pipefail 实现更安全的失败处理,使用ShellCheck获得快速反馈。这些习惯使高级Bash脚本更易于维护,并在自动化运行中大大减少意外。