crontab 日期命令报 unexpected EOF
带 date 日期格式的命令在终端正常,放进 crontab 却报 unexpected EOF。未转义的百分号可能让 cron 提前截断命令;逐个转义,或把日期与日志重定向一起移入脚本。
命令在终端里能执行,放进 crontab 后却报括号或引号没有闭合,按日期命名的日志文件也没有生成:
unexpected EOF while looking for matching ')'
syntax error: unexpected end of file
一种容易忽略的情况,是任务里直接写了 date +%Y%m%d。对于 Vixie cron、Cronie 这类把 % 用作标准输入分隔符的实现,cron 可能在把命令交给 shell 前就截断了它。此时换引号、补括号,甚至换成 Bash,都没有处理掉截断的原因。
下面针对这类 Linux cron 实现。先用本机的 man 5 crontab 确认百分号规则,不要把它套到 systemd timer、应用内调度器或只借用了 cron 时间表达式的服务上。路径 /home/alice 是示例,需换成任务所属用户实际可访问的目录。
看命令是否在第一个百分号处断开
下面这行想在每小时整点追加一条日志,但日期格式中的百分号尚未转义:
0 * * * * /bin/echo ok >> "/home/alice/cron-logs/run-$(/usr/bin/date +%Y%m%d-%H).log" 2>&1
cron 先处理命令字段中的百分号。遇到第一个没有转义的 % 时,它把前面的部分当作要执行的命令,把后面的内容作为标准输入交给命令;后续未转义的百分号还会转换为换行。
因此,shell 得到的命令已经停在 date + 附近,后面的格式字符、右括号和引号不再属于命令文本。shell 报未闭合,并不意味着原来那整行真的少写了右括号。
用反引号写命令替换时,错误可能变成“找不到匹配的反引号”;使用 $(...) 时,则可能提示找不到右括号。不同 shell 的报错措辞也会不同。它们都只能提示解析失败,仍需对照 crontab 中第一个未转义的百分号,不能仅凭 unexpected EOF 就认定是这个原因。
普通单引号和双引号只能影响后面的 shell 解析,不能保护这一层的 %。如果连日期文件都没有出现,也不要急着归因于目录权限:日期展开和重定向可能根本还没执行成功。
直接写在 crontab 中时,逐个转义百分号
把命令字段中需要原样保留的每个 % 写成 \%:
0 * * * * /bin/echo ok >> "/home/alice/cron-logs/run-$(/usr/bin/date +\%Y\%m\%d-\%H).log" 2>&1
cron 处理这行时会去掉保护百分号的反斜杠,shell 和 date 最终拿到正常的日期格式。不要只转义第一个 %Y,后面的 %m、%d、%H 同样需要处理。
这里的 %Y%m%d-%H 表示年、月、日和小时,使用 date 当时的时区。%d 只是月份中的日期,并不包含小时;如果希望按小时分文件,格式中应包含 %H。调度所用时区与进程中 date 使用的时区也应按部署环境核对。
示例假定 /bin/echo、/usr/bin/date 在这些位置,且 cron-logs 目录已存在、任务用户有写入权限。路径加双引号是为了保护 shell 展开后的文件名,这与 cron 层的反斜杠各有作用。>> 追加输出,2>&1 再将标准错误送到同一目标。
该行用于用户自己的 crontab -e,五个时间字段后直接写命令。若写进 /etc/crontab 或 /etc/cron.d/ 下的系统任务文件,还需要在命令前填写执行用户。不要把系统任务格式里的用户名复制进个人 crontab。
命令复杂时,把日期和重定向一起移入脚本
一行里同时存在日期格式、管道、算术表达式或多层引号时,用脚本更容易检查。比如保存为 /home/alice/bin/hourly-log.sh:
#!/bin/sh
set -eu
logdir=/home/alice/cron-logs
stamp=$(/usr/bin/date +%Y%m%d-%H)
exec >> "$logdir/run-$stamp.log" 2>&1
/bin/echo ok
脚本中的日期格式使用普通 %,不用为了 cron 写成 \%。cron 读取的是调度行,不会再把脚本文件的内容按 crontab 命令字段重新切分。
对应的个人 crontab 只保留脚本调用和一个固定的启动错误日志:
0 * * * * /bin/sh /home/alice/bin/hourly-log.sh >> /home/alice/cron-logs/launcher.log 2>&1
外层的 launcher.log 用于接收脚本成功切换到日期日志之前的输出,例如脚本文件无法读取、日期命令失败或日期日志无法打开。脚本执行到 exec 并完成重定向后,后续输出进入按日期命名的文件。
这里显式使用 /bin/sh 读取脚本,所以脚本需要可读,不依赖可执行位。父目录仍需允许任务用户访问。如果业务脚本依赖 Bash 语法,应明确使用实际的 Bash 路径和相符的脚本写法;换 shell 本身并不能绕过 crontab 命令字段里的百分号处理。
要把日期表达式和相关重定向一起移入脚本。仅把业务命令放进脚本,却仍在 crontab 末尾写未转义的日期文件名,问题还会留在调度行里。
区分 shell 检查和 cron 验证
修改前保存当前 crontab 的完整内容,通过 crontab -e 只调整目标任务,保留其他任务。不要为了增加一行就用一个单行文件替换整张表。保存后用下面的命令确认安装进去的内容:
crontab -l
若采用脚本方式,可先只检查 shell 语法:
/bin/sh -n /home/alice/bin/hourly-log.sh
语法检查不会执行脚本,也不能验证目录权限、日期命令是否存在或 cron 是否正确调度。反过来,终端里执行原始命令成功,也不能证明 crontab 写法正确,因为终端没有经过 cron 的百分号处理。
在本地测试环境中,用只追加 ok 的示例验证即可,不必直接反复触发备份、清理或通知等有副作用的业务任务。使用实际任务用户、相同目录和相同 cron 实现,等待计划时间后,检查日期文件名、追加内容及 launcher.log。临时提高测试频率时记录原值,验证后恢复,避免把一次小时任务留成每分钟运行。
如果没有任何文件,先区分任务是否被调度、命令是否成功解析、目录是否可写。cron 产生的未重定向输出可能按本机邮件配置处理;未配置邮件投递时,不能把“没收到邮件”当成没有报错。也不要一开始就把输出全部丢到空设备,否则会失去这次解析失败的线索。
其他命令里的百分号也受同一规则影响
这项规则不专属于 date。直接写在 crontab 命令字段中的 printf 格式、带百分号的 URL,以及 shell 算术取模符号,都可能触发同样的切分。只有本来就打算用百分号向命令提供标准输入时,才应保留这种特殊用途。
修正时区分百分号所在的位置:调度行里需要字面百分号的地方转义,脚本文件里按该脚本语言的正常语法保留。需要回退时恢复本次任务条目或脚本的备份,并核对频率、执行用户和日志位置,不必删除整张 crontab。


