0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

cron の「13日の金曜日」は書けない。日と曜日は OR で、しかも */2 は * 扱いだった

0
Posted at

背景

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補助で執筆した、個人開発の紹介記事です。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?