每个工程师都写过"临时跑一下"的脚本。它们中的大多数死了,少数活着——活着的那些往往成了系统里最危险的部分:没人敢动、没人敢删、只在出事时被想起。脚本从一次性变成资产,差的是五条底线。

底线一:参数化,不写死

# ❌ 资产负债
rsync -av /data/photos/ backup-server:/backup/photos/

# ✅ 同样十行,可复用
#!/usr/bin/env bash
set -euo pipefail
SRC="${1:?用法: $0 <src> <dest>}"
DEST="${2:?缺少目标}"
rsync -av --delete "$SRC" "$DEST"

路径、阈值、主机名全部参数化。判断标准:换个环境,脚本能原样跑。

底线二:set -euo pipefail

bash 默认的容错是灾难级的:命令失败继续往下跑,变量拼错当空串。这三个开关是 shell 脚本的疫苗:

set -euo pipefail
# -e   任何命令失败立即退出
# -u   引用未定义变量报错
# -o pipefail  管道中任何一环失败则整体失败

底线三:幂等

脚本会被跑第二遍——人手滑、cron 重启、你忘了跑没跑。写的时候就假设它会重复执行:

  • 建目录用 mkdir -p 不用 mkdir
  • 下载前判断目标存在(或校验哈希)
  • 数据库操作用 INSERT ... ON CONFLICT / 先查后插
  • 破坏性步骤(删除、覆盖)放最后,且先备份

底线四:日志与退出码

cron 里的哑巴脚本是最差的组合。最低配置:

log() { printf '%s [%s] %s\n' "$(date +%F\ %T)" "$*" >> "$LOG_FILE"; }
trap 'log "失败于行 $LINENO"' ERR
log "备份完成: $(du -sh "$DEST" | cut -f1)"

退出码是给机器看的(调度器判断成败),日志是给人看的(定位为什么)。两者都要。

底线五:头部三行文档

# 用途: 每日增量备份照片卷到备用机
# 用法: backup-photos.sh <src> <dest>
# 依赖: rsync; cron: 0 3 * * *

三个月后的你 equals 一个陌生人。这三行是写给他的。

什么时候升级成"真正的程序"

信号:超过 200 行 / 需要测试 / 要处理复杂数据结构 → 换 Python(Go)。shell 的甜蜜区是"粘合命令",超出这个区,它的每一个特性(字符串即数组、全局作用域、无异常)都在跟你作对。

一次性代码的悲剧不是写得快,而是它总活得太久。要么按标准写,要么跑完就删——最怕的是中间态。