exec >>script.log 2>&1脚本开头一行,之后所有 stdout/stderr 都写进日志文件。
约 1363 字大约 5 分钟
2026-05-14
手动跑脚本,出错了你能看到屏幕输出。但脚本一旦交给 cron 或 systemd 定时跑,它在你看不见的地方执行——没有日志,出了问题你连“它跑没跑、跑到哪一步、为什么失败”都无从知道。给脚本加日志和错误处理,本质是“让看不见的执行变得可排查”。
给脚本加上带时间戳的日志、把输出重定向到日志文件、用 trap 在出错时保留现场,让定时执行的脚本变得可排查。
exec >>script.log 2>&1脚本开头一行,之后所有 stdout/stderr 都写进日志文件。
logger -t myjob "done"把消息送进 journald/syslog,用 journalctl 就能查。
trap cleanup EXIT无论正常还是异常退出,都执行清理函数。
trap "echo err line $LINENO" ERR命令失败时打印出错的行号,保留现场。
一条有用的脚本日志,应该让人能回答:做到哪一步了、什么时候、成没成。所以最基本的日志函数:
log() {
echo "[$(date '+%F %T')] $*"
}
log "开始备份 $src"
# ... 实际操作 ...
log "备份完成,耗时 ${SECONDS}s"加时间戳是关键——定时任务出问题时,“它是几点跑的、卡在哪一步花了多久”往往就是破案线索。只 echo "done" 而不带时间和上下文,等于没记。
定时脚本的输出没人盯着,要主动写进文件。在脚本开头加一行:
#!/usr/bin/env bash
exec >>/var/log/myjob.log 2>&1
# 这行之后,脚本所有的 stdout 和 stderr 都追加进 myjob.logexec >>file 2>&1 的意思是“从此刻起,重定向本脚本的全部输出”。2>&1 把错误流也并进去——你既要正常日志,更要错误日志,出问题时全靠它(重定向原理见 管道与重定向)。
如果脚本是 systemd 管理的,可以不自己写文件,直接 echo 到 stdout,journald 会自动收走,用 journalctl -u 查(见 日志系统基础)。
trap 让脚本在特定事件发生时执行一段代码:
# 无论怎么退出,都清理临时文件
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
# 命令出错时,打印出错的行号
trap 'echo "[ERROR] 第 $LINENO 行执行失败" >&2' ERRtrap ... EXIT —— 脚本退出时(不管正常还是异常)执行,最适合做清理(删临时文件、释放锁)。trap ... ERR —— 有命令失败时执行,适合保留现场(记下出错位置、当时的变量值)。最该避免的反模式,是让错误“悄无声息地溜过去”:
some_command 2>/dev/null # 把错误丢进黑洞——出问题你永远不知道
rm -rf "$dir" || true # 失败也当没事——下游可能基于错误状态继续跑错误处理的目标是保留现场,而不是消灭现场。命令可能失败,就检查它的退出码、记下日志、决定要不要中止——而不是把 stderr 重定向到 /dev/null 假装没发生。配合 set -euo pipefail,能让“被忽略的错误”自动暴露出来。
定时脚本没有日志 = 出了事也查不到
这是定时任务最大的坑:手动跑好好的脚本,放进 cron 后某天默默失败了好几周,因为没人看、也没日志。上线一个定时脚本前,务必确认:① 它的输出有地方可查(写文件或进 journald);② 失败时退出码非 0,且日志里能看出失败原因;③ 关键步骤有带时间戳的进度日志。日志文件本身也要纳入轮转(logrotate),否则它自己会把磁盘写满。
| 误区 | 更稳妥的做法 |
|---|---|
| 定时脚本不写任何日志 | 至少把输出 exec >>file 2>&1,或交给 journald |
日志只 echo "done" 不带时间和上下文 | 加时间戳和“做到哪一步”,出事才有线索 |
用 2>/dev/null 把错误丢掉 | 错误要保留现场:检查退出码、记日志、决定是否中止 |
| 只记 stdout,漏掉 stderr | 2>&1 把错误流也收进日志,出问题主要靠它 |
版权归属:Shuo Liu