crontab 里的 % 会被当成换行:一个静默失败的命令
这个坑的特征是:命令在终端里手敲一切正常,写进 crontab 就默默失败,日志里连个错都没有。原因往往是 %。
一、% 在 crontab 里是换行符
crontab 的命令部分,% 有特殊含义:它等价于回车换行,后面第一个 % 之后的内容会被当成 stdin 喂给命令。
所以这条:
0 3 * * * tar -czf /backup/db-$(date +%Y%m%d).tar.gz /data
实际执行的结果是:tar -czf /backup/db-$(date + 被当成命令,Y%m%d)... 被当成标准输入。命令直接语法错误,退出码非零,但因为没重定向,错误只发到 crontab 的邮件里,而多数系统没配邮件,于是"什么都没发生"。
正确写法是把 % 转义:
0 3 * * * tar -czf /backup/db-$(date +\%Y\%m\%d).tar.gz /data
注意是 \%,不是 %%。
二、比转义更省事的办法:把逻辑挪进脚本
我的做法是把整个备份流程写成一个脚本文件,crontab 里只调脚本:
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
脚本里想怎么写 % 都行,还能加错误处理、加锁、加通知。crontab 越短,踩特殊字符坑的概率越低。
三、同源的第二个坑:PATH 不一样
crontab 执行时用的是 bash 的非交互非登录模式,不读 /etc/profile、不读 ~/.bashrc。所以你在终端里能用的 node、ffmpeg、docker,在 cron 里可能直接 command not found。
两种解法:
# 法一:在 crontab 顶部显式声明
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
# 法二:脚本里用绝对路径
/usr/local/bin/node /opt/scripts/task.js
我倾向法二,因为法一在每个用户、每台机器上都得重配一遍,容易漏。
四、还没结束:环境变量也是干净的
LANG=zh_CN.UTF-8
五、并发重叠:上一次还没跑完,这一次又来了
cron 不会管上一次是否结束。如果任务耗时超过间隔,就会出现多个实例同时跑,互相踩文件。用 flock 加个锁:
*/5 * * * * flock -n /var/lock/mytask.lock /opt/scripts/task.sh
六、验证方法
写进 crontab 之后别急着等。先把时间设成下一分钟,然后盯日志:
# 临时改成每分钟,验证完再改回来
* * * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
另一个技巧:在脚本开头加一行调试输出,把关键环境打印出来。第一次跑完看一眼就知道环境对不对:
#!/bin/bash
echo "=== $(date) PATH=$PATH LANG=$LANG TZ=$TZ PWD=$(pwd)"
速查表
| 症状 | 大概率原因 | 修法 |
|---|---|---|
| 完全不执行,无报错 | 命令里有 % | 转义成 \% 或挪进脚本 |
command not found | PATH 不含该命令 | 用绝对路径 |
| 中文乱码 / 编码异常 | 没有 LANG | crontab 里声明 LANG |
| 时间戳差 8 小时 | 没有 TZ | 声明 TZ=Asia/Shanghai |
| 同一任务跑了多份 | 没有互斥 | flock -n |
- 点赞
- 收藏
- 关注作者
评论(0)