Bash 自动化脚本最佳实践:错误处理、日志记录、参数解析与定时任务封装 Bash 自动化脚本最佳实践错误处理、日志记录、参数解析与定时任务封装Bash 是 Linux 运维工程师最常用的脚本语言几乎所有 Linux 发行版都内置支持无需额外安装依赖。然而许多工程师写出的 Bash 脚本缺乏健壮性——遇到错误默默继续执行、日志散落无法追溯、参数解析依赖位置变量而难以维护。这类脚本在开发环境运行顺畅一旦进入生产环境就容易引发难以排查的故障。本教程聚焦 Bash 脚本的防御性编程实践覆盖set -euo pipefail安全模式、trap错误捕获、getopts参数解析、tee日志双输出、lockfile 防重复执行等核心技术并提供一个可直接用于生产环境的完整运维脚本模板。无论是数据库备份、日志归档还是服务健康检查掌握这些技巧都能让你的脚本从能跑就行升级为生产可信赖。环境要求Bash 脚本对服务器资源要求极低运维脚本本身几乎不占用计算资源主要消耗来自脚本所操作的目标服务数据库、日志系统等。推荐使用雨云服务器 rainyun-com注册填优惠码2026off领 5 折优惠券。选择1 核 2GB 机型搭配 Ubuntu 22.04 LTS 或 Debian 12Bash 版本 5.x完全满足日常运维脚本的执行需求。该配置性价比极高适合搭建个人运维中控机或作为定时任务服务器长期运行。操作系统Ubuntu 22.04 / Debian 12 / CentOS 7Bash 4.x 以上Bash 版本4.0推荐 5.x支持关联数组等高级特性必备工具coreutils、util-linux系统默认安装可选工具mailutils或msmtp用于告警邮件、jqJSON 处理安装准备确认 Bash 版本bash--version# GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)安装辅助工具# Ubuntu / Debiansudoaptupdatesudoaptinstall-yjq mailutils msmtp# CentOS / RHELsudoyuminstall-yjq mailx创建脚本目录结构mkdir-p/opt/scripts/{bin,lib,log,lock,conf}chmod750/opt/scripts推荐的目录约定目录用途/opt/scripts/bin/可执行脚本/opt/scripts/lib/公共函数库/opt/scripts/log/运行日志/opt/scripts/lock/锁文件目录/opt/scripts/conf/配置文件核心操作防御性模式set -euo pipefail这是所有生产脚本必须放在第一行的安全套件#!/usr/bin/env bashset-euopipefailIFS$\n\t各选项含义选项作用-e任意命令返回非零退出码时立即退出-u使用未定义变量时报错退出防止$UNDEF静默为空-o pipefail管道中任意命令失败时整个管道返回非零IFS$\n\t避免文件名中含空格时 for 循环错误分割反面示例不加pipefail的陷阱# 以下命令在没有 pipefail 时会成功退出因为 grep 返回 0cat/nonexistent_file|grepsomethingechoexit code:$?# 输出 0掩盖了 cat 的错误trap 错误捕获trap可以在脚本异常退出时执行清理动作防止留下临时文件或锁文件#!/usr/bin/env bashset-euopipefailSCRIPT_NAME$(basename$0)LOG_FILE/opt/scripts/log/${SCRIPT_NAME%.sh}_$(date%Y%m%d).logLOCK_FILE/opt/scripts/lock/${SCRIPT_NAME%.sh}.lockTMP_DIR# 清理函数脚本退出时自动调用cleanup(){localexit_code$?# 删除临时目录[[-n$TMP_DIR-d$TMP_DIR]]rm-rf$TMP_DIR# 释放锁文件[[-f$LOCK_FILE]]rm-f$LOCK_FILEif[[$exit_code-ne0]];thenlogERROR脚本异常退出退出码:$exit_codefi}# 注册 trapEXIT正常/异常退出、INTCtrlC、TERMkill 信号trapcleanup EXIT INTTERMtee 日志双输出同时将日志写入文件和输出到终端并自动带上时间戳# 日志函数log(){locallevel$1localmessage$2localtimestamptimestamp$(date%Y-%m-%d %H:%M:%S)echo[${timestamp}] [${level}]${message}|tee-a$LOG_FILE}# 将整个脚本的 stdout/stderr 都重定向到日志同时保留终端输出exec(tee-a$LOG_FILE)21使用exec (tee -a ...)后脚本中所有echo、命令输出都会同时写入日志文件。getopts 参数解析使用内置getopts而非位置参数$1 $2支持标准 Unix 风格的选项usage(){catEOF 用法:$(basename$0)[选项] 选项: -h 显示帮助信息 -e ENV 运行环境 (dev|staging|prod)默认: dev -d DIR 备份目标目录 -r DAYS 保留天数默认: 7 -v 详细输出模式 示例:$(basename$0)-e prod -d /backup -r 30 EOF}# 默认值ENVdevBACKUP_DIR/tmp/backupRETAIN_DAYS7VERBOSEfalse# 解析选项whilegetopts:he:d:r:vopt;docase$optinh)usage;exit0;;e)ENV$OPTARG;;d)BACKUP_DIR$OPTARG;;r)RETAIN_DAYS$OPTARG;;v)VERBOSEtrue;;:)echo选项 -$OPTARG需要参数;exit1;;\?)echo未知选项: -$OPTARG;exit1;;esacdoneshift$((OPTIND-1))# 参数校验[[$ENV~^(dev|staging|prod)$]]||{echo无效环境:$ENV;exit1;}[[$RETAIN_DAYS~^[0-9]$]]||{echo保留天数必须为正整数;exit1;}lockfile 防重复执行对于定时任务必须防止前一个实例还在运行时启动新实例acquire_lock(){# 使用 mkdir 创建锁原子操作避免竞态条件if!mkdir$LOCK_FILE2/dev/null;then# 检查持有锁的进程是否还在运行localpid_file${LOCK_FILE}/pidif[[-f$pid_file]];thenlocalpidpid$(cat$pid_file)ifkill-0$pid2/dev/null;thenlogWARN脚本已在运行中 (PID:$pid)退出exit0elselogWARN发现过期锁文件PID$pid已不存在清理后继续rm-rf$LOCK_FILEmkdir$LOCK_FILEfififiecho$$${LOCK_FILE}/pidlogINFO获取锁成功 (PID:$$)}进阶配置公共函数库将通用函数抽取到/opt/scripts/lib/common.sh各脚本 source 引用# /opt/scripts/lib/common.sh# 检查命令是否存在require_cmd(){localcmd$1command-v$cmd/dev/null21||{echo错误必需命令 $cmd 未安装2exit1}}# 检查是否以 root 运行require_root(){[[$EUID-eq0]]||{echo此脚本需要 root 权限2;exit1;}}# 发送告警邮件send_alert(){localsubject$1localbody$2localto${ALERT_EMAIL:-rootlocalhost}echo$body|mail-s[告警]$subject$to}# 重试机制retry(){localmax_attempts$1localdelay$2shift2localattempt1while[[$attempt-le$max_attempts]];doif$;thenreturn0filogWARN命令失败第$attempt/$max_attempts次重试等待${delay}ssleep$delay((attempt))donereturn1}在脚本中引用# shellcheck source/opt/scripts/lib/common.shsource/opt/scripts/lib/common.sh require_cmd jq require_root日志自动轮转在/etc/logrotate.d/ops-scripts中添加/opt/scripts/log/*.log { daily rotate 30 compress delaycompress missingok notifempty dateext }自动化方案完整运维脚本模板以下是可直接使用的生产级备份脚本模板#!/usr/bin/env bash# # 脚本名称: db_backup.sh# 描述: MySQL 数据库自动备份脚本# 用法: ./db_backup.sh -e prod -d /backup/mysql -r 30# set-euopipefailIFS$\n\t# ── 常量 ──────────────────────────────────────────────────────────────────────readonlySCRIPT_DIR$(cd $(dirname${BASH_SOURCE[0]})pwd) readonly SCRIPT_NAME$(basename$0.sh) readonly LOG_DIR/opt/scripts/log readonly LOCK_DIR/opt/scripts/lock readonly LOG_FILE${LOG_DIR}/${SCRIPT_NAME}_$(date%Y%m%d).log readonly LOCK_FILE${LOCK_DIR}/${SCRIPT_NAME}.lock # ── 默认配置 ────────────────────────────────────────────────────────────────── ENVdev BACKUP_DIR/backup/mysql RETAIN_DAYS7 DB_HOSTlocalhost DB_PORT3306 DB_USERbackup_user DB_PASS# ── 初始化 ────────────────────────────────────────────────────────────────────mkdir-p$LOG_DIR$LOCK_DIRexec(tee-a$LOG_FILE)21# ── 函数定义 ──────────────────────────────────────────────────────────────────log(){echo[$(date%Y-%m-%d %H:%M:%S)] [$1]$2;}cleanup(){localcode$?[[-d$LOCK_FILE]]rm-rf$LOCK_FILE[[$code-ne0]]logERROR脚本异常退出 (code$code)logINFO 脚本结束 }trapcleanup EXIT INTTERMacquire_lock(){if!mkdir$LOCK_FILE2/dev/null;thenlocalpid;pid$(cat${LOCK_FILE}/pid2/dev/null||echounknown)kill-0$pid2/dev/null{logWARN已在运行 (PID$pid)退出;exit0;}rm-rf$LOCK_FILEmkdir$LOCK_FILEfiecho$$${LOCK_FILE}/pid}usage(){echo用法:$(basename$0)[-e ENV] [-d DIR] [-r DAYS] [-h]exit0}parse_args(){whilegetopts:e:d:r:hopt;docase$optine)ENV$OPTARG;;d)BACKUP_DIR$OPTARG;;r)RETAIN_DAYS$OPTARG;;h)usage;;:)logERROR选项 -$OPTARG缺少参数;exit1;;\?)logERROR未知选项: -$OPTARG;exit1;;esacdone}do_backup(){localbackup_file${BACKUP_DIR}/db_${ENV}_$(date%Y%m%d_%H%M%S).sql.gzlogINFO开始备份 →$backup_filemysqldump--host$DB_HOST--port$DB_PORT\--user$DB_USER--password$DB_PASS\--all-databases --single-transaction--quick\|gzip$backup_filelogINFO备份完成大小:$(du-sh$backup_file|cut-f1)}cleanup_old(){logINFO清理${RETAIN_DAYS}天前的备份find$BACKUP_DIR-name*.sql.gz-mtime${RETAIN_DAYS}-deletelogINFO清理完成}# ── 主流程 ────────────────────────────────────────────────────────────────────main(){logINFO 脚本启动 (PID$$, ENV$ENV) acquire_lockmkdir-p$BACKUP_DIRdo_backup cleanup_old logINFO所有任务执行成功}parse_args$main加入 crontab 定时执行# 编辑 crontabcrontab-e# 每天凌晨 2:30 执行备份将错误单独记录302* * * /opt/scripts/bin/db_backup.sh-eprod-d/backup/mysql-r30/opt/scripts/log/cron.log21注意crontab 中的PATH与交互式 Shell 不同建议在脚本顶部显式设置exportPATH/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin常见问题Qset -e导致脚本在预期失败的命令处意外退出怎么办对于允许失败的命令使用|| true或条件判断# 方式1允许失败greppatternfile.txt||true# 方式2用 if 判断ifgrep-qpatternfile.txt;thenecho找到了fiQ脚本中如何安全地存储密码绝对不要将密码硬编码在脚本中。推荐方案# 方式1从配置文件读取限制权限chmod600/opt/scripts/conf/db.confsource/opt/scripts/conf/db.conf# 文件中 DB_PASSxxx# 方式2使用环境变量在 crontab 中设置DB_PASS$(cat/run/secrets/db_password)Q如何调试 Bash 脚本# 开启调试模式显示每条执行的命令bash-x/opt/scripts/bin/db_backup.sh# 或在脚本中局部开启set-x# ... 需要调试的代码 ...setx# 检查语法但不执行bash-nscript.shQgetopts和getopt有什么区别getopts是 Bash 内置命令可移植性好只支持短选项-egetopt外部命令支持长选项--env但不同发行版行为有差异。纯可移植脚本推荐使用getopts。Q如何让脚本在后台运行并且退出终端后不中断# 方式1nohupnohup/opt/scripts/bin/db_backup.sh# 方式2systemd oneshot 服务推荐生产使用# 方式3screen / tmux健壮的 Bash 脚本是 Linux 运维自动化的基石。从set -euo pipefail到trap清理、从getopts到 lockfile每一个细节都在为生产稳定性保驾护航。想要一台稳定、低成本的服务器来跑这些定时任务和运维脚本推荐雨云服务器 rainyun-com的1 核 2GB 机型注册时填入优惠码2026off可领取 5 折优惠券是搭建个人运维中控机的性价比之选。