Skip to content

日常运维脚本实例

运维中常见的脚本场景包括服务巡检、磁盘检查、文件备份和批量操作。编写时重点放在参数校验、错误处理、退出码、日志输出和危险操作保护。

一、脚本骨架

一个可复用的基本结构:

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 防止单台机器网络不通时拖住整批任务的执行。

批量操作的风险控制:先跑只读命令(hostnameuptimedf -hsystemctl 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 +x

set -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)能让代码更清晰,错误提示也更友好。