高级 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)

此模式适用于 commdiff 以及需要多个文件输入的工具。

语法变体:

  • <(command) 将命令输出传递给读取器。
  • >(command) 将写入的输出发送到另一个命令。

3. 添加严格模式和ShellCheck

高级Bash脚本应在发生意外时大声失败。严格模式有助于在静默损坏发生前捕获缺失变量和损坏的管道。

基本严格模式选项

大多数自动化脚本应以以下内容开头:

set -euo pipefail
  1. -e 命令失败时退出。
  2. -u 将未设置的变量视为错误。
  3. -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

使用参数扩展进行内联清理

参数扩展可以在简单情况下清理字符串,而无需启动 sedawk

  • ${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脚本更易于维护,并在自动化运行中大大减少意外。