这是《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
建议:
- 显式
#!/usr/bin/env bash; - 使用
set -euo pipefail; - 参数必须校验;
- 外部依赖显式声明;
- 输出适合日志采集。
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 或编程语言,并保留审计和测试。
思考题
set -e和set -o pipefail分别解决什么问题?$@为什么要加引号?- 为什么临时文件推荐
mktemp? - 如何限制脚本并发数?
- 设计一个支持健康检查和回滚的发布脚本。