DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

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。