背景
cron 式を貼ると次の実行時刻を並べてくれるツールを、自分の Web ツール集に置いています。
https://hashitosystem.com/tools/cronparse/
cron は昔から「なんとなく書いて、動いたらよし」で済ませていて、仕様をちゃんと読んだことがありませんでした(反省しています…)。
今回ツールの判定を cronie のソースと突き合わせたら、自分のツールが1か所だけ本家と違う答えを出していたので、その話を書きます。
cron 式のおさらい
cron は、決まった時刻にコマンドを動かす Unix の仕組みだ。
設定ファイル crontab の1行は、空白区切りの5つのフィールドとコマンドで書きます。
分 時 日 月 曜日 コマンド
0 9 13 * 5 /path/to/{実行したいコマンド}
曜日は 0 が日曜で、5 が金曜です(7 も日曜として受け付けられます)。
* は「全部」、1,15 は列挙、1-5 は範囲、*/2 は「全範囲から2つおき」を意味します。
ここで出てくる cronie は、cron の実装のひとつです。
Linux の man ページを集めた man7.org の crontab(5) も、cronie のものが載っています。
https://man7.org/linux/man-pages/man5/crontab.5.html
日と曜日は OR
上の 0 9 13 * 5 は、素直に読むと「13日の金曜日の9時」に見えます。
でも crontab(5) には、日と曜日の両方が制限されている(* ではない)ときは、どちらか一方が一致すれば実行する、と書かれています。
man ページ自身の例も 30 4 1,15 * 5 で、「毎月1日と15日、それに加えて毎週金曜の4時30分」という意味だそうです。
実際に、ツールの判定部分を node に写して、2026年10月1日から次の実行日を6つ出してみました。
== 0 9 13 * 5
2026-10-02 金, 2026-10-09 金, 2026-10-13 火, 2026-10-16 金, 2026-10-23 金, 2026-10-30 金
13日の火曜日にも、13日でない金曜日にも動いているのがわかると思います。
「13日の金曜日だけ」は cron 式だけでは書けない、ということだ。
どうしても必要なら、0 9 13 * * で毎月13日に起動して、コマンドの先頭で [ "$(date +\%u)" = 5 ] && のように曜日を見て抜けるしかなさそうです(crontab のコマンド部分では % が改行扱いになるので \% にします)。
*/2 は * 扱いになる
ここまでは知っている人も多い気がします。
私がハマったのはこの先です。
cronie のソースでは、日と曜日の照合は下記の式になっています(src/cron.c)。
(((e->flags & DOM_STAR) || (e->flags & DOW_STAR))
? (bit_test(e->dow, dow) && bit_test(e->dom, dom))
: (bit_test(e->dow, dow) || bit_test(e->dom, dom)))
DOM_STAR は「日フィールドが * だった」、DOW_STAR は「曜日フィールドが * だった」という印で、どちらかが立っていれば AND、両方立っていなければ OR です。
問題はこの印の立て方で、src/entry.c では下記のようにフィールドの1文字目だけを見ています。
if (ch == '*')
e->flags |= DOM_STAR;
ch = get_list(e->dom, FIRST_DOM, LAST_DOM, PPC_NULL, ch, file);
ch はそのフィールドの最初の1文字で、ステップ(/2)を読むのは後ろの get_list です。
なので */2 も先頭が * なので「* だった」扱いになり、AND の側に入ります。
https://github.com/cronie-crond/cronie/blob/master/src/entry.c
一方、私のツールは p[2].trim() === '*' のように「フィールド全体が * そのものか」で判定していました。
0 9 */2 * 1 を両方のルールで比べたのが下記です(exact が私のツール、first が cronie の判定)。
== 0 9 */2 * 1
exact: 2026-10-01 木, 2026-10-03 土, 2026-10-05 月, 2026-10-07 水, 2026-10-09 金, 2026-10-11 日
first: 2026-10-05 月, 2026-10-19 月, 2026-11-09 月, 2026-11-23 月
私のツールは OR で読んでいるので「奇数日か、月曜」になり、ほぼ毎日動く予定を出していました。
cronie は AND なので「奇数日の月曜」だけで、10月と11月の2か月で4回しか動きません。
同じ1行で、実行回数がまるで違います…
比べるのに使った判定は下記です。
exact と first はフィールドの文字列を受け取って「* 扱いか」を返す関数で、ここだけ差し替えています。
const exact = (f) => f.trim() === '*'; // 全体が * のときだけ(私のツール)
const first = (f) => f.trim()[0] === '*'; // 先頭が * なら(cronie)
function dayMatches(c, d) {
const domOk = c.dom.has(d.getDate());
const dowOk = c.dow.has(d.getDay()) || (d.getDay() === 0 && c.dow.has(7));
return (c.domStar || c.dowStar) ? (domOk && dowOk) : (domOk || dowOk);
}
c.dom と c.dow は日と曜日のフィールドを数値の Set に展開したもの、d は判定する時刻の Date で、c.domStar には exact(日フィールド) か first(日フィールド) の結果が入ります。
これを1分ずつ進めながら呼んで、一致した時刻を集めたのが上の出力です。
なぜ cronie に合わせるのか
「*/2 は * ではない」という私の読みも、文面だけなら筋は通っている気がします。
ただ、cron 式のツールが答えるべきなのは「実際の cron がいつ動かすか」です。
仕様の文章とソースが食い違うなら、ユーザーの手元で動いているのはソースのほうなので、そちらに合わせるべきだと思います。
cron の実装は他にもあって全部がこうとは限らないので、ここで言えるのは cronie ではこう動く、というところまでです。
日と曜日の両方に値を書く cron 式は、OR になることを前提に組んでください。
そして */2 のように * で始まるフィールドは、cronie では * と同じ扱いになって AND に切り替わる、と覚えておくのが安全です。
本記事はAI補助で執筆した、個人開発の紹介記事です。