cronの日と曜日はOR照合 — */2始まりだとANDになる理由
crontab の 5 つのフィールドのうち、分・時・月は「すべて一致したら実行」という素直な AND で照合される。ところが 3 番目の「日」と 5 番目の「曜日」だけは、両方を指定すると OR になる。第 1 月曜のつもりで 0 9 1-7 * 1 と書くと、1〜7 日の毎日と、月曜すべてで動いてしまう。
さらに、どちらかが「指定なし」かどうかの判定は実装によって細部が違う。この記事では、ハシトシステムの Cron 式パーサ の実装を題材に、POSIX の規定と cronie(Fedora や RHEL 系で使われる cron デーモン)のソースを読み、*/2 のようにアスタリスクで始まる式で AND と OR が入れ替わる仕組みを Node.js で確かめる。
POSIX の規定: 両方が「要素かリスト」なら OR
POSIX(IEEE Std 1003.1-2024)の crontab は、日付側の照合を 4 通りに分けて書いている。
| 日(と月) | 曜日 | 一致する日 |
|---|---|---|
* |
* |
毎日 |
| 指定あり | * |
日の指定に一致する日 |
* |
指定あり | 曜日の指定に一致する日 |
| 指定あり | 指定あり | どちらかに一致する日 |
ここでいう「指定あり」は、数か範囲(1-7 のようなハイフンでつないだ 2 つの数)をカンマで並べたもので、POSIX の文法にはステップ(/2)が無い。*/2 をどう扱うかは POSIX の外、実装の判断になる。
cronie の crontab(5) も同じ規則を例付きで説明していて、30 4 1,15 * 5 は毎月 1 日と 15 日の 4:30 に加えて、毎週金曜の 4:30 にも動く、としている。
cronie は「先頭の 1 文字が * か」で決める
cronie の src/entry.c は、日と曜日のフィールドを読み始める直前に、最初の 1 文字だけを見てフラグを立てる。
1 | |
曜日側も同じ形で DOW_STAR を立てる。照合する src/cron.c は、このフラグでどちらの式を使うかを切り替える。
1 | |
どちらかのフラグが立っていれば AND、どちらも立っていなければ OR である。直前のコメントには、この扱いは奇妙だが標準がそうなっている、という趣旨の一文が添えられている。
ここで効いてくるのが、フラグが「フィールド全体が * か」ではなく「先頭が * か」で決まる点だ。*/2 は先頭が * なので DOM_STAR が立ち、AND で照合される。一方、同じ奇数日を表す 1-31/2 は先頭が 1 なのでフラグが立たず、OR で照合される。集合としては同じ日を指していても、書き方で動く日が変わる。
なお crontab(5) の本文は「* を含まないとき」を指定ありとしているが、上のコードが見ているのは先頭の 1 文字だけなので、1,*/2 のように途中に * がある式はコード上は指定ありとして扱われる。
自作の照合で「全体が * か」を見ると逆になる
cron 式を自分で解析するコードでは、指定なしの判定を field === '*' と書きがちである。ハシトシステムの Cron 式パーサも以前はそうなっていて、*/2 を指定ありとして OR で照合していた。現在は cronie に合わせて先頭 1 文字で判定している。
1 | |
1 | |
両方の判定方法で、2026 年 11 月のどの日に動くかを並べると違いがはっきりする(下の「実際に試す」のスクリプトの出力)。
| 日 * 曜日 | 先頭で判定(cronie) | 全体で判定 |
|---|---|---|
1,15 * 5 |
6 日(1・15 日と金曜) | 6 日 |
1-7 * 1 |
11 日(1〜7 日と月曜) | 11 日 |
*/2 * 1 |
2 日(奇数日かつ月曜) | 18 日(奇数日か月曜) |
* * 1 |
5 日(月曜) | 5 日 |
1-31/2 * 1 |
18 日 | 18 日 |
*/2 * 1 だけが、判定方法によって 2 日と 18 日に分かれる。cronie の上で動く設定を自作のツールで検証していると、この 1 行で次回実行日の表示が食い違う。
実際に試す
Node.js 24.11.1(Windows 11)で実行した。依存パッケージは要らない。次の内容を cron-days.mjs として保存する。
1 | |
1 | |
出力:
1 | |
曜日は UTC の暦で数えているので、手元のタイムゾーンに関係なく同じ結果になる。2026 年 11 月 1 日は日曜で、1-7 * 1 は 1〜7 日の 7 日と、9・16・23・30 日の月曜 4 日を足した 11 日になる。第 1 月曜(2 日)だけで動かしたいなら、この書き方は使えない。
第 1 月曜に動かす書き方
cron の文法だけでは「1〜7 日かつ月曜」を表せないので、日の側で 1〜7 日に絞り、曜日はコマンドの中で確かめる。
1 | |
曜日のフィールドを * にしておけば、日は 1-7 だけで照合される(表の 2 行目)。date +%u は月曜を 1、日曜を 7 で返すので、1 のときだけ後ろのコマンドが動く。手元の GNU date で確かめた結果は次のとおりで、2026 年 11 月の 1〜7 日のうち 1 を返すのは 2 日だけである。
1 | |
crontab の行では % を \% と書く。POSIX の規定では、コマンド欄の % はエスケープしないと改行に置き換えられ、それ以降は標準入力として渡される。date +%u をそのまま書くと $(date + の直後で行が切れ、シェルの構文として成り立たなくなる。また、コマンド欄は /bin/sh で実行されるので、[[ のような bash の構文は使えない(「crontabではbinshで実行される」)。
実装するときの注意
- 日と曜日を両方指定すると OR になる。
1-7 * 1は第 1 月曜ではなく、1〜7 日と全月曜で動く。AND にしたいときは片方を*にして、残りをコマンド側で判定する。 - cronie は先頭の 1 文字で判定する。
*/2は*扱いで AND、同じ集合の1-31/2は OR。ステップは POSIX の文法に無い拡張なので、この挙動は実装依存として扱う。 - 自作のパーサは
field === '*'で判定しない。cronie の上で動く crontab を検証するなら、先頭 1 文字で判定しないと次回実行日がずれる。 - コマンド欄の
%はエスケープする。date +%uはdate +\%uと書く。
まとめ
- POSIX は、日と曜日がどちらも数・範囲・リストで指定されたとき、どちらかに一致する日を実行日とする。
- cronie は
DOM_STAR/DOW_STARをフィールドの先頭 1 文字で立て、どちらかが立てば AND、どちらも立たなければ OR で照合する。*/2 * 1は 2026 年 11 月に 2 日、全体一致で判定する実装では 18 日と、10 日以上の差が出る。 - 第 1 月曜のような条件は cron の文法だけでは書けない。
1-7 * *とdate +\%uの判定を組み合わせる。
ツール: Cron式パーサ(ハシトシステム)。cron 式を日本語で説明し、次回の実行日時を並べる。日と曜日の両方を指定したときは、AND と OR のどちらで照合されるかも表示する。式を組み立てる側は Cron式ビルダー で作れる。