LinuxNotes

第 12 章:Shell 脚本

zjc 于 2026-01-12 发布

这是《Linux 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Shell 脚本把命令、判断、循环和函数组合成可重复执行的自动化流程。好的脚本能减少误操作;坏脚本可能把一次小故障放大成全网事故。本章聚焦 Bash 的可靠写法、参数校验、错误处理、日志、并发和发布脚本设计。

12.1 第一个脚本

创建 healthcheck.sh

#!/usr/bin/env bash

set -euo pipefail

url="${1:?Usage: $0 http://host:port/healthz}"
timeout="${2:-3}"

curl --silent --show-error --fail \
     --connect-timeout "$timeout" \
     --max-time "$timeout" \
     "$url" >/dev/null

echo "OK $url"

执行:

chmod 750 healthcheck.sh
./healthcheck.sh http://127.0.0.1:8080/healthz

建议:

  1. 显式 #!/usr/bin/env bash
  2. 使用 set -euo pipefail
  3. 参数必须校验;
  4. 外部依赖显式声明;
  5. 输出适合日志采集。

12.2 set 选项

选项 作用
-e 命令失败时退出
-u 未定义变量报错
-o pipefail 管道中任一命令失败则整条失败
-x 打印执行命令

组合:

set -euo pipefail

调试:

bash -x script.sh

在脚本内开启:

set -x
set +x

set -e 对条件命令、命令替换和某些复合命令有细节差异。关键逻辑应显式判断退出码。

12.3 变量与参数

变量:

app_name="order-service"
readonly app_name
port="${APP_PORT:-8080}"
echo "$app_name:$port"

参数:

变量 含义
$0 脚本名
$1$9 参数
${10} 第十个参数
$# 参数数量
$@ 所有参数,推荐加引号
$* 所有参数合并为单字符串

示例:

for arg in "$@"; do
  echo "$arg"
done

安全默认值:

env_name="${ENV_NAME:?ENV_NAME is required}"
timeout="${TIMEOUT:-5}"

12.4 判断

数值比较:

if (( timeout > 10 )); then
  echo "timeout too large"
fi

字符串:

if [[ "$env" == "prod" ]]; then
  echo "production"
fi

文件:

表达式 含义
-e file 存在
-f file 普通文件
-d file 目录
-r file 可读
-w file 可写
-x file 可执行
-s file 非空

示例:

conf="/etc/app/app.yml"
if [[ ! -r "$conf" ]]; then
  echo "cannot read $conf" >&2
  exit 1
fi

[[ ]] 是 Bash 推荐写法;跨 POSIX Shell 应使用 test[ ]

12.5 循环与函数

循环:

for host in web01 web02 web03; do
  ssh "$host" hostname
done

for i in {1..5}; do
  echo "$i"
done

while read -r line; do
  echo "$line"
done < hosts.txt

函数:

log() {
  printf '%s [%s] %s\n' "$(date -Is)" "$1" "$2"
}

wait_http() {
  local url="$1"
  local retry="${2:-30}"
  local i

  for ((i = 1; i <= retry; i++)); do
    if curl -sf --max-time 2 "$url" >/dev/null; then
      log INFO "$url ready"
      return 0
    fi
    sleep 1
  done

  log ERROR "$url not ready"
  return 1
}

函数内局部变量使用 local,避免污染全局状态。

12.6 命令替换与退出码

命令替换:

current_dir=$(pwd)
today=$(date +%F)
files=$(find . -type f | wc -l)

退出码:

if command -v jq >/dev/null 2>&1; then
  echo "jq found"
else
  echo "jq not found" >&2
  exit 127
fi

显式处理:

if ! mkdir -p "$backup_dir"; then
  echo "mkdir failed: $backup_dir" >&2
  exit 1
fi

12.7 输入与临时文件

读取 stdin:

while read -r line; do
  printf '%s\n' "$line"
done

读取配置:

source /etc/app/env

source 会执行文件内容,只能加载可信文件。配置解析优先使用专用工具。

临时文件:

tmpdir=$(mktemp -d)
cleanup() {
  rm -rf "$tmpdir"
}
trap cleanup EXIT

不要使用 /tmp/app.$$ 这种可预测路径写敏感文件。

12.8 并发控制

简单后台任务:

pids=()
for host in web01 web02 web03; do
  ssh "$host" sudo systemctl reload nginx &
  pids+=("$!")
done

for pid in "${pids[@]}"; do
  wait "$pid" || exit 1
done

限制并发:

max_parallel=5
semaphore="$tmpdir/sem"
mkfifo "$semaphore"
exec 3<>"$semaphore"

for ((i = 0; i < max_parallel; i++)); do
  printf '\n' >&3
done

for host in $(cat hosts.txt); do
  read -r _ <&3
  {
    ssh "$host" hostname
    printf '\n' >&3
  } &
done
wait

也可以使用 xargs -P、GNU parallel 或 Ansible。并发要设置批次和超时,避免把目标系统打满。

12.9 远程执行

ssh app@web01 'hostname'
scp app.tar.gz app@web01:/tmp/
rsync -av --delete --exclude logs/ release/ app@web01:/opt/app/current/

批量执行建议:

while read -r host; do
  ssh -o BatchMode=yes -o ConnectTimeout=5 \
      "deploy@${host}" \
      'sudo systemctl restart app'
done < hosts.txt

当脚本超过远程控制、幂等、错误传播、并发和审计这些能力时,应迁移到 Ansible。

12.10 幂等与回滚

幂等示例:

ensure_dir() {
  local dir="$1"
  if [[ ! -d "$dir" ]]; then
    mkdir -p "$dir"
  fi
}

发布脚本骨架:

#!/usr/bin/env bash
set -euo pipefail

app_dir=/opt/app
release_id="${1:?release id required}"
release_dir="$app_dir/releases/$release_id"
current="$app_dir/current"

[[ -d "$release_dir" ]] || { echo "release not found" >&2; exit 1; }

ln -sfn "$release_dir" "$app_dir/current.next"
mv -T "$app_dir/current.next" "$current"
sudo systemctl restart app

if ! curl -sf --max-time 3 http://127.0.0.1:8080/healthz; then
  ln -sfn "$app_dir/previous" "$app_dir/current"
  sudo systemctl restart app
  exit 1
fi

实际发布还要处理版本记录、串行批次、服务注册中心摘流和人工确认。

12.11 脚本测试与审计

语法检查:

bash -n script.sh
shellcheck script.sh

推荐工具:

工具 用途
bash -n 语法检查
shellcheck 静态分析
shfmt 格式化
bats 脚本测试

审计要求:

[ ] 脚本在仓库中管理
[ ] 变更有评审
[ ] 生产先 dry-run
[ ] 支持回滚
[ ] 输出带时间日志
[ ] 不输出明文密码
[ ] 锁定适用主机范围
[ ] 保留执行记录

本章小结

可靠脚本的核心是显式、可预期、可重复、可回滚。使用 set -euo pipefail、参数校验、局部变量、函数、trap 清理、超时和日志。复杂度上升后,应把 Shell 脚本迁移到 Ansible 或编程语言,并保留审计和测试。

思考题

  1. set -eset -o pipefail 分别解决什么问题?
  2. $@ 为什么要加引号?
  3. 为什么临时文件推荐 mktemp
  4. 如何限制脚本并发数?
  5. 设计一个支持健康检查和回滚的发布脚本。