休日に実行する・しないcronの簡単な支援スクリプト

  • 8
    Like
  • 4
    Comment

crontab で休日に実行したくない場合

0 12 * * 1-5 xtetsuji program.sh

といったように、5番目の曜日指定部分で 1-5 つまり月曜日から金曜日と指定することはよく行われます。

ただ、この方法では祝日に対応できない、というわけで職業プログラマー人生で定期的に書いては捨てている気がするプログラムを今回も書いてみました。

  • 注:このスクリプトで想定している date コマンドは、多くの Linux ディストリビューションでインストールされる GNU coreutils に付属しているものです。BSD系など(macOSも)では仕様が若干違うことで、そのままではスクリプトが動作しないかもしれません。
is-holiday.sh
#!/bin/bash

case "_$1" in
    "_-h"|"_--help")
        perldoc $0
        exit
        ;;
esac

set -eu

year=$( LANG=C date +"%Y" )
month=$( LANG=C date +"%m" )
day=$( LANG=C date +"%d" )
month=${month#0}
day=${day#0}
month_day="$month/$day"

weekday=$( LANG=C date +"%w" )
nth_weekday_in_month=$(( ($day - 1) / 7 + 1 ))

month_nth_weekday="${month}_${nth_weekday_in_month}_${weekday}"

EXIT_CODE_HOLIDAY=0
EXIT_CODE_WORKDAY=1

# 土日は休日
case $weekday in
    0|6|7)
        exit $EXIT_CODE_HOLIDAY
        ;;
esac

# 祝日を列挙(固定日)
case $month_day in
    1/[123]|2/11|4/29|5/[345]|8/11|11/3|11/23|12/23)
        exit $EXIT_CODE_HOLIDAY
        ;;
esac

# 祝日を列挙(第n月曜日系)
case $month_nth_weekday in
    1_2_1)
        # 1月第2月曜日は成人の日
        exit $EXIT_CODE_HOLIDAY
        ;;
    7_3_1)
        # 7月第3月曜日は海の日
        exit $EXIT_CODE_HOLIDAY
        ;;
    9_3_1)
        # 9月第3月曜日は敬老の日
        exit $EXIT_CODE_HOLIDAY
        ;;
    10_2_1)
        # 10月第2月曜日は体育の日
        exit $EXIT_CODE_HOLIDAY
        ;;
esac

# その他ある?
# 自社の休暇などもここで列挙
if [ $year = 2017 ] ; then
    if [ $month_day = "3/20" ] || [ $month_day = "9/18" ] ; then
        # 2017年の秋分の日と春分の日
        exit $EXIT_CODE_HOLIDAY
    fi
fi
# TODO: 春分の日や秋分の日を割り出し(holidays.icalをどこからか取得して解析?)

# そうでなければ働く日
exit $EXIT_CODE_WORKDAY

: <<END_POD

=pod

=encoding utf-8

=head1 NAME

is-holiday.sh - 休日かどうか

=head1 SYNOPSIS

  is-holiday.sh ; echo $?
  is-holiday.sh || some-weekday-batch.sh

=head1 DESCRIPTION

このプログラムを実行すると、休日だと判定される場合は終了コード 0 で正常終了、休日だと判定されない場合は(つまり働く日の場合は)終了コード 1 で非正常終了します。

なので、 || や && といった条件演算子と組み合わせることによって、cron の中で祝日判定をすることなく、営業日のみ実行するプログラムをしかけたりすることができます。

=head1 MODIFICATION GUIDLINE

祝日定義を追加する際には以下のようにするとよいでしょう。

  # 祝日を列挙(固定日)
  case $month_day in
      1/[123]|2/11|4/29|5/[345]|8/11|11/3|11/23|12/23)

固定の祝日は この条件節に入るので、例えば3月3日を祝日にする場合は 3/3 を縦棒で区切ってこの中に入れると良いです。

  # 祝日を列挙(第n月曜日系)
  case $month_nth_weekday in
      1_2_1)

最近多い「第2月曜日」系については、その下にある case 文に入れます。

${m} 月、第 ${n} 月曜日の場合は ${m}_${n}_1 という書き方になります。月曜日は 1。日曜日は 0 か 7 (date コマンドの実装依存?)ですが、火曜日から土曜日は 2 から 6 となります。大抵は月曜日のはずなので _1 で良いはず。

=head1 TODO

春分の日と秋分の日の動的判定。どこかから holidays.ical を取ってくればいいんだけど、シェルスクリプトだと少々面倒。

=head1 SEE ALSO

L<Wikipedia - 国民の祝日|https://ja.wikipedia.org/wiki/%E5%9B%BD%E6%B0%91%E3%81%AE%E7%A5%9D%E6%97%A5>

=head1 AUTHOR

Initial written by OGATA Tetsuji E<lt>tetsuji.ogata@gmail.comE<gt>.

=cut

END_POD

少しずつシェルスクリプト力が成長しているので、数年前より多少は洗練されているような気がします。

使い方はソースコードの中に書かれている通りです。コマンドからだと is-holiday.sh --help などとすることで見ることができる親切設計。

端的に書けば

is-holiday.sh && 祝日や休日に実行したいスクリプト
is-holiday.sh || 営業日に実行したいスクリプト

なのですが、せっかくなので上記シェルスクリプトで使われているテクニックを解説します。

個別のシェルスクリプトテクニック

ヘルプと perldoc

case "_$1" in
    "_-h"|"_--help")
        exec perldoc $0
        ;;
esac

ヘルプを出力するために -h もしくは --help という引数が第1引数にあるか検査する部分。

シェルスクリプトは大部分の構文がコマンドとして実行されているので、文字列を受け取る時にハイフンで始まる文字は不都合があることが多く、冒頭のハイフンを隠すような書き方が好まれます。上記では _ を使っていますが、古いシェルスクリプトでは x を使っている例が多く見受けられます。

bash の case 文は、単純な文字列合致や OR 条件を if 文より簡単に書くことができるので便利です。

上記では -h もしくは --help 引数を指定すると exec perldoc $0 を実行することになります。bash での exec コマンドは、このシェルスクリプトのプロセスを破棄して(正確にはメモリ的に置き換えて)引数に指定されたコマンドを実行するというもの。なので、以後は perldoc $0 コマンドとなります。

$0 は自分自身のファイルパスに展開されるので、 perldoc コマンドに自分自身を与えることになります。

: <<END_POD

=pod

=encoding utf-8

perldoc コマンドは =pod から =cut までの間を POD フォーマットで書かれていると認識して man コマンドのようにレンダリングしてくれるコマンドです。

: コマンドの意味については、以下の記事を参照下さい。

set -eu

シェルスクリプトも良いスタイルを維持したいので、Perl でいう use strictuse warnings のような構文の厳格チェック set -eu を入れます。

-e は errexit とも呼ばれ、シェルスクリプトのコマンドの中で終了コードが 1 以上のものがあればそこで停止します。逆を言えば、これを指定しないシェルスクリプトは終了コードが 1 以上、つまり(多くの場合)エラーとなったコマンドがあっても後続のコマンドが実行されます。この動作が好ましくない場合は多いでしょう。

-u は nounset とも呼ばれ、未定義の変数を参照できないようにする機能です。代入を行ったことがない変数が参照できないという意味では、Ruby のデフォルトの変数参照と同様と解釈してよいでしょう。

注意としては set -u を書いてしまうと、第1引数が指定されない場合に $1 と書いてしまうと、その時点でエラーとなってしまうこと。なので $1 チェックの後に書いています。

もし set -u を書いた後に $1 を参照したい場合は ${1:-} と書くか set +u で nounset を一度無効化する必要があるでしょう。

date コマンドとフォーマット

year=$( LANG=C date +"%Y" )

date コマンドは + の後にフォーマット文字列を伴うことでパーツを受け取ることができます。上記では2017年に実行すると 2017 という4文字を受け取ります。

LANG=C 部分は、 これがないと date +"%m" で Jul が返ってきたりとたまに驚くことが起こるので指定しています。

month=${month#0}
day=${day#0}
month_day="$month/$day"

月も日も printf でいうと %02d 的フォーマットで得られるので、頭のゼロを bash の変数参照の方法で除去しています。こうしておかないと、数値演算の時に8進数だと思われてしまうことがあり、それを気にする場合は除去を励行しておくと良いでしょう。

$ echo $(( 1 + 07 ))
8
$ echo $(( 1 + 08 ))
bash: 1 + 08: value too great for base (error token is "08")

曜日

weekday=$( LANG=C date +"%w" )
nth_weekday_in_month=$(( ($day - 1) / 7 + 1 ))

month_nth_weekday="${month}_${nth_weekday_in_month}_${weekday}"

曜日は date コマンドで %w というフォーマットから得られます。

LANG=C を指定していれば、得られる値は crontab のそれとだいたい同じで、1から6 で 月曜日から土曜日です。日曜日は 0 または 7 で得られ、それは実装依存となっているようです(理解曖昧)。

最近の祝日を考えるとき、「第n月曜日」ということを考える必要がありますが、「第いくつ」かは、少し考えると上記のような計算式で計算できます。bash の数値計算は $(( ... )) で行えること、また / は割り算の商のみで小数点演算はしないことに注意。

定数で終了コードに名前をつける

EXIT_CODE_HOLIDAY=0
EXIT_CODE_WORKDAY=1

「祝日の場合は0だっけ…」というのは悩みがち(特に Perl や PHP と「真偽値」が逆なので)なので、最初から名前をつけておくとよいですね。