Appearance
日常运维脚本实例
运维中常见的脚本场景包括服务巡检、磁盘检查、文件备份和批量操作。编写时重点放在参数校验、错误处理、退出码、日志输出和危险操作保护。
一、脚本骨架
一个可复用的基本结构:
bash
#!/usr/bin/env bash
set -euo pipefail
log_info() {
printf '[INFO] %s\n' "$*"
}
log_error() {
printf '[ERROR] %s\n' "$*" >&2
}
require_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
log_error "missing command: $cmd"
exit 1
fi
}
main() {
require_command systemctl
log_info "script started"
# 主逻辑放在这里
}
main "$@"骨架各部分的作用:
| 部分 | 作用 |
|---|---|
set -euo pipefail | 失败、未定义变量、管道中间失败都尽早暴露 |
log_info / log_error | 统一日志格式,错误走 stderr |
require_command | 脚本启动时检查依赖,不在中间才因缺命令失败 |
main "$@" | 主逻辑入口清晰,原始参数完整传入 |
脚本很短时不一定需要函数化。超过几十行后有一个 main 函数收纳主流程,阅读和维护都更清晰。
二、服务巡检脚本
检查指定服务是否正在运行:
bash
#!/usr/bin/env bash
set -euo pipefail
services=(nginx sshd crond)
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
printf '[OK] %s running\n' "$service"
else
printf '[FAIL] %s not running\n' "$service" >&2
fi
done带退出码的版本——有任何一个服务异常则脚本返回失败,方便监控系统识别:
bash
#!/usr/bin/env bash
set -euo pipefail
services=(nginx sshd crond)
failed=0
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
printf '[OK] %s running\n' "$service"
else
printf '[FAIL] %s not running\n' "$service" >&2
failed=1
fi
done
exit "$failed"巡检脚本的输出是给人看的([OK] / [FAIL]),退出码是给监控系统看的。两者不能互相替代——输出可以详细,但退出码必须是 0 或非 0 的明确信号。
三、磁盘使用率检查
检查根分区的使用率是否超过阈值:
bash
#!/usr/bin/env bash
set -euo pipefail
threshold="${1:-80}" # 默认阈值 80%
mount_point="/"
usage="$(
df -P "$mount_point" |
awk 'NR == 2 {gsub("%", "", $5); print $5}'
)"
if [ "$usage" -ge "$threshold" ]; then
printf '[WARN] %s usage %s%% >= %s%%\n' "$mount_point" "$usage" "$threshold" >&2
exit 1
fi
printf '[OK] %s usage %s%%\n' "$mount_point" "$usage"df -P 使用 POSIX 标准输出格式,脚本解析时比默认格式更稳定。awk 中先用 gsub 去掉 % 符号,再取纯数字进行比较。
扩展到检查所有挂载点:
bash
#!/usr/bin/env bash
set -euo pipefail
threshold="${1:-80}"
failed=0
while read -r usage mount_point; do
usage="${usage%%%}"
if [ "$usage" -ge "$threshold" ]; then
printf '[WARN] %s usage %s%%\n' "$mount_point" "$usage" >&2
failed=1
fi
done < <(df -P | awk 'NR > 1 {print $5, $6}')
exit "$failed"usage="${usage%%%}" 用参数展开删除末尾的 %,比再起一个 sed 进程更轻量。
四、备份脚本
将指定目录打包备份到本地:
bash
#!/usr/bin/env bash
set -euo pipefail
src_dir="${1:-}"
backup_root="/backup"
today="$(date +%F)"
if [ -z "$src_dir" ] || [ ! -d "$src_dir" ]; then
echo "usage: $0 <source-dir>" >&2
exit 1
fi
mkdir -p "$backup_root"
name="$(basename "$src_dir")"
archive="$backup_root/${name}-${today}.tar.gz"
tar -czf "$archive" -C "$(dirname "$src_dir")" "$name"
echo "backup created: $archive"tar -C 先切换到源目录的父目录,再打包目标目录。这样归档内不会包含一长串绝对路径,恢复时目录结构更干净。
加上保留策略——删除 7 天前的旧备份:
bash
find /backup -type f -name "*.tar.gz" -mtime +7 -print
# 确认匹配范围正确后再加上 -delete
# find /backup -type f -name "*.tar.gz" -mtime +7 -print -delete删除备份的操作需要保守。先只打印匹配结果,人工确认范围没问题,再打开 -delete。路径、后缀、时间条件都明确写清楚,多留几份备份比误删的代价小得多。
五、批量操作
从主机列表文件逐行读取,对每台机器执行指定命令:
bash
#!/usr/bin/env bash
set -euo pipefail
host_file="${1:-hosts.txt}"
cmd="${2:-hostname}"
if [ ! -f "$host_file" ]; then
echo "host file not found: $host_file" >&2
exit 1
fi
while IFS= read -r host; do
[ -n "$host" ] || continue # 跳过空行
[[ "$host" == \#* ]] && continue # 跳过注释行
echo "===== $host ====="
ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" "$cmd"
done < "$host_file"BatchMode=yes 禁止 SSH 进入交互式密码提示——如果密钥认证失败就直接报错退出,不卡住脚本。ConnectTimeout=5 防止单台机器网络不通时拖住整批任务的执行。
批量操作的风险控制:先跑只读命令(hostname、uptime、df -h、systemctl status)确认主机列表和连接都正确,确认无误后再执行变更命令(重启服务、改配置、清理文件)。
六、临时文件和清理
脚本中需要临时空间时,用 mktemp 创建唯一的临时目录,避免和固定路径冲突:
bash
#!/usr/bin/env bash
set -euo pipefail
tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT # 不管脚本成功还是失败退出,都执行清理
echo "work dir: $tmp_dir"
curl -fsS -o "$tmp_dir/index.html" https://example.com/
wc -c "$tmp_dir/index.html"mktemp -d 在 /tmp 下创建一个唯一的随机名称目录,避免多实例并发时路径冲突。trap cleanup EXIT 保证脚本无论正常结束还是中途出错退出,都会运行 cleanup 函数删除临时文件。
七、日志和调试
脚本在 cron 或 systemd timer 中运行时,可以将所有输出统一追加到日志文件:
bash
#!/usr/bin/env bash
set -euo pipefail
log_file="/var/log/check-demo.log"
exec >>"$log_file" 2>&1 # 后续所有 stdout/stderr 都追加到日志
echo "[$(date '+%F %T')] script started"
hostname
df -h /
echo "[$(date '+%F %T')] script finished"exec >>"$log_file" 2>&1 将当前 Shell 进程的 stdout 和 stderr 都重定向到指定文件。cron 中运行的脚本用这种方式集中输出,比每行命令单独加重定向更整洁。
调试时查看脚本的执行过程:
bash
bash -x script.sh在脚本内部临时打开跟踪:
bash
set -x
systemctl status nginx --no-pager
set +xset -x 会打印每条命令及其展开后的变量值。需要注意调试日志可能包含密码、token、密钥路径等敏感信息——保存和分享调试日志前先检查内容。
八、参数解析
用 case 实现简单的命令行选项解析:
bash
#!/usr/bin/env bash
set -euo pipefail
dry_run=0
target=""
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--target)
target="${2:-}"
shift 2
;;
-h|--help)
echo "usage: $0 [--dry-run] --target <path>"
exit 0
;;
*)
echo "unknown option: $1" >&2
exit 1
;;
esac
done
if [ -z "$target" ]; then
echo "missing required option: --target" >&2
exit 1
fi
if [ "$dry_run" -eq 1 ]; then
echo "dry run: would process $target"
else
echo "processing $target"
fi当命令行参数变多、选项变复杂(长选项、短选项、子命令、互斥选项),Shell 里的手写 case 解析会越来越吃力。这种时候换用 Python 或 Go 写 CLI 工具,参数解析库(argparse、cobra)能让代码更清晰,错误提示也更友好。