手动跑得好好的脚本,一进 cron 就失败:PATH、工作目录和 % 的三个坑
同一个脚本,登录终端执行一切正常,写进 crontab 之后就开始报错。绝大多数情况不是 cron 有 bug,而是它给出的执行环境和你的交互终端根本不是一回事。下面按三个坑的顺序给定位命令和修法。
一、cron 的 PATH 短到超乎想象
cron 启动任务时不会读 ~/.bashrc、~/.profile 或 /etc/profile,所以你在 shell 配置里 export 的变量一个都拿不到,PATH 通常只剩 /usr/bin:/bin。凡是装在 /usr/local/bin、/opt 下面,或者依赖 conda、nvm、go 环境的命令,都会以退出码 127 加一行 command not found 收场。
定位办法很直接,先塞一行探针把环境打到日志里,再跑真活:
* * * * * /bin/sh -c 'echo PATH=$PATH; echo HOME=$HOME; command -v python3; pwd' >> /tmp/cron-probe.log 2>&1
确认是 PATH 之后有两种修法。省事的是在 crontab 顶部显式赋值,它对同一文件里的所有任务生效:
PATH=/usr/local/bin:/usr/bin:/bin SHELL=/bin/bash 0 3 * * * /home/app/backup.sh
更稳的是脚本里所有外部命令一律写绝对路径,不依赖任何隐式查找。两个一起做最好。
还要注意 SHELL 本身。Debian 系上 cron 默认用 /bin/sh,而它是 dash,不支持双方括号、数组和部分 bash 扩展语法。脚本首行写了 shebang 也未必管用:如果 crontab 里是以 sh script.sh 的形式调起来,shebang 就被绕过了。给脚本加执行权限、crontab 里直接写路径本身,才会按 shebang 走。
二、工作目录不是你以为的那个
cron 执行任务时的工作目录是用户的家目录,不是脚本所在目录。于是脚本里所有相对路径都会指向错误位置,症状通常是文件找不到,或者产物莫名其妙出现在家目录里。修法是开头显式切目录,并且让失败可见:
cd "$(dirname "$0")" || exit 1
这一行顺带解决了从不同目录调用脚本、行为不一致的老问题。
三、百分号在 crontab 里是元字符
这是最隐蔽的一个。crontab 语法里未转义的百分号表示命令结束、后面接的是喂给进程的 stdin,所以 date +%Y%m%d 会被从百分号处截断,实际执行的命令只剩一个 date +。必须写成转义形式:
0 1 * * * /usr/local/bin/backup.sh > /var/log/backup-$(date +\%Y\%m\%d).log 2>&1
嫌转义难读的话,把日期计算挪进脚本、crontab 里只留一条最简调用,是更清爽的做法。
四、另外两件容易被忽略的事
其一是重叠执行:上一轮还没跑完,下一轮又起来了,两个进程互相踩数据。cron 自己不防重,用 flock 包一层就够了,拿不到锁直接退出:
/usr/bin/flock -n /var/lock/backup.lock /home/app/backup.sh
其二是时区。cron 按系统本地时区解释时间字段,容器里尤其容易和宿主机不一致。需要固定时区时,RHEL 系可以在 crontab 顶部写 CRON_TZ=Asia/Shanghai;或者改用 systemd timer,它按 UTC 存储、按本地时区显示,systemctl list-timers 能直接看到下一次触发的时间。
五、一条排查顺序
遇到 cron 失败,按这个顺序走基本不会漏:先把输出重定向到日志,确认它到底有没有被触发;再看退出码,127 是命令找不到、126 是不可执行、1 是脚本内部错误;然后在家目录和脚本目录两处分别找产物,判断是不是相对路径问题;最后把整条命令用 crontab 同样的 SHELL 和 PATH 手工复现一遍:
env -i PATH=/usr/bin:/bin HOME=/home/app /bin/sh -c '你的命令'
能在这条命令里复现出来的故障,就都是环境问题而不是代码问题。
小结:cron 只保证按时执行,不保证按你的环境执行。绝对路径、开头显式 cd、转义百分号这三件事做完,八成的定时任务故障会消失;剩下的两成交给 systemd timer 的日志和状态查询来收。
- 点赞
- 收藏
- 关注作者
评论(0)