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
2
3
if (ch == '*')
e->flags |= DOM_STAR;
ch = get_list(e->dom, FIRST_DOM, LAST_DOM, PPC_NULL, ch, file);

曜日側も同じ形で DOW_STAR を立てる。照合する src/cron.c は、このフラグでどちらの式を使うかを切り替える。

1
2
3
(((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))

どちらかのフラグが立っていれば AND、どちらも立っていなければ OR である。直前のコメントには、この扱いは奇妙だが標準がそうなっている、という趣旨の一文が添えられている。

ここで効いてくるのが、フラグが「フィールド全体が * か」ではなく「先頭が * か」で決まる点だ。*/2 は先頭が * なので DOM_STAR が立ち、AND で照合される。一方、同じ奇数日を表す 1-31/2 は先頭が 1 なのでフラグが立たず、OR で照合される。集合としては同じ日を指していても、書き方で動く日が変わる。

なお crontab(5) の本文は「* を含まないとき」を指定ありとしているが、上のコードが見ているのは先頭の 1 文字だけなので、1,*/2 のように途中に * がある式はコード上は指定ありとして扱われる。

自作の照合で「全体が * か」を見ると逆になる

cron 式を自分で解析するコードでは、指定なしの判定を field === '*' と書きがちである。ハシトシステムの Cron 式パーサも以前はそうなっていて、*/2 を指定ありとして OR で照合していた。現在は cronie に合わせて先頭 1 文字で判定している。

1
domStar:p[2].trim().charAt(0)==='*',dowStar:p[4].trim().charAt(0)==='*',
1
2
if(c.domStar||c.dowStar)dayOk=domOk&&dowOk;
else dayOk=domOk||dowOk;

両方の判定方法で、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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// cron-days.mjs : 日と曜日の組み合わせで、どの日に動くかを列挙する
// starMode: 'cronie' = 先頭1文字が * なら「* 扱い」(cronie の entry.c と同じ)
// 'exact' = フィールド全体が * のときだけ「* 扱い」(よくある自作実装)
function parseField(f, min, max) {
const set = new Set();
for (const part of f.split(',')) {
const [range, stepStr] = part.split('/');
const step = stepStr === undefined ? 1 : Number(stepStr);
let lo, hi;
if (range === '*') [lo, hi] = [min, max];
else if (range.includes('-')) [lo, hi] = range.split('-').map(Number);
else lo = hi = Number(range);
if (![lo, hi, step].every(Number.isInteger) || step < 1 || lo < min || hi > max || lo > hi) {
throw new Error(`bad field: ${f}`);
}
for (let v = lo; v <= hi; v += step) set.add(v);
}
return set;
}

export function dayMatcher(domField, dowField, starMode) {
const dom = parseField(domField, 1, 31);
const dow = parseField(dowField, 0, 7); // 0 と 7 はどちらも日曜
const isStar = (f) => (starMode === 'cronie' ? f[0] === '*' : f === '*');
const and = isStar(domField) || isStar(dowField);
return (date) => {
const d = date.getUTCDay();
const domOk = dom.has(date.getUTCDate());
const dowOk = dow.has(d) || (d === 0 && dow.has(7));
return and ? domOk && dowOk : domOk || dowOk;
};
}

const W = ['日', '月', '火', '水', '木', '金', '土'];
function daysIn(year, month, match) {
const out = [];
for (let day = 1; day <= 31; day++) {
const dt = new Date(Date.UTC(year, month - 1, day));
if (dt.getUTCMonth() !== month - 1) break;
if (match(dt)) out.push(`${day}(${W[dt.getUTCDay()]})`);
}
return out;
}

const cases = [
['1,15', '5'], // 1日と15日、それに金曜
['1-7', '1'], // 「第1月曜」のつもり
['*/2', '1'], // 奇数日の月曜のつもり
['*', '1'],
['1-31/2', '1'], // */2 と同じ集合だが * で始まらない
];
for (const [dom, dow] of cases) {
for (const mode of ['cronie', 'exact']) {
const days = daysIn(2026, 11, dayMatcher(dom, dow, mode));
console.log(`${`${dom} * ${dow}`.padEnd(12)} ${mode.padEnd(6)} ${String(days.length).padStart(2)}日: ${days.join(' ')}`);
}
}
1
node cron-days.mjs

出力:

1
2
3
4
5
6
7
8
9
10
1,15 * 5     cronie  6日: 1(日) 6(金) 13(金) 15(日) 20(金) 27(金)
1,15 * 5 exact 6日: 1(日) 6(金) 13(金) 15(日) 20(金) 27(金)
1-7 * 1 cronie 11日: 1(日) 2(月) 3(火) 4(水) 5(木) 6(金) 7(土) 9(月) 16(月) 23(月) 30(月)
1-7 * 1 exact 11日: 1(日) 2(月) 3(火) 4(水) 5(木) 6(金) 7(土) 9(月) 16(月) 23(月) 30(月)
*/2 * 1 cronie 2日: 9(月) 23(月)
*/2 * 1 exact 18日: 1(日) 2(月) 3(火) 5(木) 7(土) 9(月) 11(水) 13(金) 15(日) 16(月) 17(火) 19(木) 21(土) 23(月) 25(水) 27(金) 29(日) 30(月)
* * 1 cronie 5日: 2(月) 9(月) 16(月) 23(月) 30(月)
* * 1 exact 5日: 2(月) 9(月) 16(月) 23(月) 30(月)
1-31/2 * 1 cronie 18日: 1(日) 2(月) 3(火) 5(木) 7(土) 9(月) 11(水) 13(金) 15(日) 16(月) 17(火) 19(木) 21(土) 23(月) 25(水) 27(金) 29(日) 30(月)
1-31/2 * 1 exact 18日: 1(日) 2(月) 3(火) 5(木) 7(土) 9(月) 11(水) 13(金) 15(日) 16(月) 17(火) 19(木) 21(土) 23(月) 25(水) 27(金) 29(日) 30(月)

曜日は 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
0 9 1-7 * * [ "$(date +\%u)" = 1 ] && /usr/local/bin/monthly-report.sh

曜日のフィールドを * にしておけば、日は 1-7 だけで照合される(表の 2 行目)。date +%u は月曜を 1、日曜を 7 で返すので、1 のときだけ後ろのコマンドが動く。手元の GNU date で確かめた結果は次のとおりで、2026 年 11 月の 1〜7 日のうち 1 を返すのは 2 日だけである。

1
2
3
4
$ for d in 2026-11-01 2026-11-02 2026-11-09; do printf '%s %s\n' "$d" "$(date -d "$d" +%u)"; done
2026-11-01 7
2026-11-02 1
2026-11-09 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式ビルダー で作れる。


cronの日と曜日はOR照合 — */2始まりだとANDになる理由
https://blog.hashito.biz/2026/10/11/cron-day-of-month-day-of-week-or-rule-step-star-cronie/
著者
hashito
作成日
2026年10月11日
著作権