[Shellコマンド] Cronによる営業日(土日・祝日以外)のみで定期コマンド実行をスケジュールしたい


2020/10/01
2022/01/22
蛸壺の技術ブログ|Cronによる営業日(土日・祝日以外)のみで定期コマンド実行をスケジュールしたい

Cronを業務で使っていると、営業日の夕方に1回、月曜・水曜・金曜の朝礼時間に1回、などなどコマンド実行時間の細かいニーズにも対応を迫られる時があります。

そんな細かいスケジューリングをCron単体で定義するのはとても大変です。

かといって実行するシェルスクリプト側に毎回営業日などを判定するロジックを組み込むのも面倒な作業です。

そこで今回はgrepコマンドと併用してCronを利用するハイブリッドな方法で、とてもスマートに土日祝日以外の日でコマンドをスケジューリングする方法を深堀りしてみます。


動作確認時の手元の環境

この記事の内容確認時の手元のLinux環境は以下のようになっています。

            
            $ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 10 (buster)
Release: 10
Codename: buster
        
grepコマンドなどの使用上の作法がLinuxディストリビューションごとに多少の違いがあります。

おおよそどのLinuxでも同じような手順でCronをスケジュールできると思います。


Cronコマンドの使用前準備 〜 grepでパターンを判定するテクニック

まずは今回のテクニックの肝となるgrepコマンドのパターン判定を復習しておきましょう。

grepは検索したい文字列を含む行を探して返すコマンドですので、

            
            $ echo 'hoge piyo fuga' | grep 'piyo'
hoge piyo fuga
        
とすれば、検索文字列を含んだ行だけを表示してくれます。

上の例のように標準出力ではなく、ファイルの中身も検索して該当の行だけを表示してくれます。

            
            $ cat << EOF > tmp.txt
hoge
piyo
fuga
EOF

$ cat tmp.txt | grep 'piyo'
piyo
        
もう少し遊んでみます。

以下の内容で、
tmp.shというシェルスクリプトを用意しておきます。

            
            #!/bin/bash

if echo "$1" | grep 'piyo' > /dev/null; then
    echo 'piyo was found.'
else
    echo 'piyo was not found.'
fi
        
そしてこのスクリプトを使って、引数の中にpiyoが見つかる・見つからないの判別できるかを試してみます。

            
            #tmp.shの実行権限を与える
$ chmod +x tmp.sh

#文字列にpiyoを含むと...
$ ./tmp.sh 'hoge hoge piyo fuga'
piyo was found.

#文字列にpiyoを含まないと...
$ ./tmp.sh 'hoge hoge fuga fuga'
piyo was not found.
        
となり文字列にパターンを含むかをgrepでチェックすることが可能となります。

ポイントは
> /dev/nullの部分で、標準出力をヌルデバイスにリダイレクトすることで、何か空ではない文字列があった場合にはヌルデバイスが動作して標準出力を処分した後にtrueを返します。

何もなかった場合には標準出力もないので、ヌルデバイスは動作せずにfalseを返すことを巧みに利用しているシェル技の一種です。

この
tmp.shをファイルでも使えるように以下のように手直しすると、

            
            #!/bin/bash

if grep 'piyo' "$1" > /dev/null; then
    echo 'piyo was found.'
else
    echo 'piyo was not found.'
fi
        
先ほど作成したtmp.txtを読ませて実行すると、

            
            $ cat tmp.txt
hoge
piyo
fuga

$./tmp.sh tmp.txt
piyo was found.
        
のように使えます。


Cronとgrepを組み合わせて営業日だけ実行できるスケジュールを作る

ここから本題に入ります。

祝日を調べるのにカレンダー片手に手書きで祝日データを作成する必要はありません。

まず
内閣府の国民の休日のウェブページから、昭和30年(1955年)から令和3年(2021年)国民の祝日(csv形式:19KB)というタイトルのリンクを踏むとcsv形式の祝日のデータベースをダウンロードして利用することが出来ます。

なお、
csvデータの直リンはここです。

このファイルの中身を見てもらえば分かるように、とても簡単な構造のデータセットになっています。

            
            $ wget -O tmp.csv https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv
$ cat tmp.csv | iconv -f SJIS
国民の祝日・休日月日,国民の祝日・休日名称
1955/1/1,元日
1955/1/15,成人の日
1955/3/21,春分の日
1955/4/29,天皇誕生日
1955/5/3,憲法記念日
#....中略
2021/10/11,スポーツの日
2021/11/3,文化の日
2021/11/23,勤労感謝の日
        
データは昭和30年からあります。

ここでは直近以降の祝日のデータだけがほしいので、2020年代のデータ以外は切り捨てて、
holidays.csvという名前でどこかローカルに保存しておきましょう。

            
            $ wget -q -O - https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv | \
    iconv -f SJIS | grep -e "^202" > holidays.csv
$ cat holidays.csv
2020/1/1,元日
2020/1/13,成人の日
#...中略
2021/11/3,文化の日
2021/11/23,勤労感謝の日
        

grepの判定と後続のコマンドを組み合わせる

前の節でgrepを用いた文字を含むかの判定の後に&&||を噛ませます。

これでワンライナーで祝日の日だけ動く・動かないコマンドができるようになります。

どういうことか例を挙げましょう。

先程の用意した
holidays.csvを使います。

たとえば2021年のこどもの日は
'2021/5/5'です。

この日がholidays.csvに記載されていれば
'2021/5/5'は間違いなく祝日となります。

            
            #2021/5/5,こどもの日
$ grep '2021/5/5' holidays.csv > /dev/null && echo 'こどもの日!'
こどもの日!

#何も起きない....
$ grep '2021/5/8' holidays.csv > /dev/null && echo 'こどもの日!'
        
先んじて解説していたgrepでのパターン判定と論理演算子&&をなかなか上手く利用しています。

ただこの例だと祝日においてだけ実行されてしまいます。

なので、祝日以外なら実行されるようにしたい場合には以下のようになります。

            
            #2021/5/5,こどもの日
#何も起きない
$ grep '2021/5/5' holidays.csv > /dev/null || echo 'こどもの日じゃない!'

#こどもの日ではない
$ grep '2021/5/8' holidays.csv > /dev/null || echo 'こどもの日じゃない!'
こどもの日じゃない!
        

寄り道〜会社の創立記念日をスケジュールに追加

会社によると本来の祝日とは違う休日扱いの日が存在していると思います。

特殊な休日を仕込みたい場合でも、holidays.csvに追加することで祝日扱いすることができます。

            
            $ echo '2021/8/4,会社の創立記念日' >> holidays.csv
        
なおこの場合、holidays.csvの中にある祝日の時系列的順序は問われません。

よって、ソートし直す必要もありません。

Cronと組み合わせる

では、以上の前知識を利用して首題の営業日(土日・祝日以外)の決まった時間に動作するスケジュールを作ってみましょう。

今回で言えば以下のような感じで利用することとなります。

            
            0 9 * * 1 grep `date "+\%Y/\%-m/\%-d"` holidays.csv > /dev/null || echo 'おはようございます。今週も張りきっていきましょう'
0 12 * * 1-5 grep `date "+\%Y/\%-m/\%-d"` holidays.csv > /dev/null || echo 'お昼休みです'
0 18 * * 1-5 grep `date "+\%Y/\%-m/\%-d"` holidays.csv > /dev/null || echo '残業は厳禁です!'
0 17 * * 5 grep `date "+\%Y/\%-m/\%-d"` holidays.csv > /dev/null || echo '今週もお疲れさまでした'
        
Cronスクリプト内でシステム時間を取得するのに、date "+\%Y/\%-m/\%-d"を使って本日が祝日ではないかholidays.csvの中を検索しています。

祝日でなかったら後段のコマンドを実行しています。

実行するのがechoだけですが、ここはご自身が所望されるスクリプトに置き換えてみてください。

見ていただければ分かる通り、grepによりスッキリとスマートにCronスケジュールが実装できています。

Cronスクリプトの'%'文字には注意

Cronでのコマンド設定する際に、そのコマンドがうっかり%文字を含んでいると意図せず動かなくなってしまうことがあります。

例えば先ほどのCronスクリプトで、以下のように設定してしまうと、bashコマンド直に叩くと動いていてもCronは実行されません。

            
            0 9 * * 1 grep `date "+%Y/%-m/%-d"` holidays.csv > /dev/null || echo 'おはようございます。今週も張りきっていきましょう'
0 12 * * 1-5 grep `date "+%Y/%-m/%-d"` holidays.csv > /dev/null || echo 'お昼休みです'
0 18 * * 1-5 grep `date "+%Y/%-m/%-d"` holidays.csv > /dev/null || echo '残業は厳禁です!'
0 17 * * 5 grep `date "+%Y/%-m/%-d"` holidays.csv > /dev/null || echo '今週もお疲れさまでした'
        
一見、このスクリプトには何処にも問題は無さそうで、うっかり見過ごしてしまいやすいと思います。

これはCron特有のもので、dateコマンドの引数の部分に注目すると、
%文字がエスケープなしで用いられているというだけで躓いていたようです。

このように
%文字はCronでシステム文字扱いされ、\(エスケープ)無しで使われると、改行文字として認識される仕様に起因するもののようです。

Cronに登録するコマンドに
%文字をきちんと識別して欲しい場合には、\%としているか確認してください。

Cronで使うための環境変数の設定方法

Cronスクリプトの中だけで使う環境変数を設定して、より柔軟にCronコマンドをスリムに書ける方法もあります。

これは著者自身も最近まであまり気にしたことは無かったので、Cronでスケジュール実行させるコマンドを指定させる場合にはフルパスで呼び出して使っていました。

            
            #...省略
* * * 2 1-5 grep `date "+\%Y/\%-m/\%-d"` /home/hoge/piyo/fuga/moga/my-favorite-desk.csv > /dev/null || bash /home/hogehoge/piyo/fugafuga/2022-02/seller.sh >> /home/hoge/piyo/fuga/moga/2022-03/sales.csv
#...省略
        
やたらとシェルコマンドファイルや入出力ファイルの保存先がバラけるので、そのリソースの置き場所を絶対パスでリンクさせると、ニョキニョキと伸びていって、何がなんだか見づらいCronタスクになります。

こういう場合に、Cronだけで使うローカルな環境変数を使いたくなります。

何も考えないと
.bashrcなどに環境変数を追加してCronで使うことを思いつきますが、実はそこまでせずとも、crontabファイルに直接書けば、ローカルな環境変数として使えるようになるとのことです。

例えば、先程のCronタスクなら、このタスクより前の適当な行で、独自のローカル環境変数を定義します。

            
            #...省略
#👇Cronスクリプト内部で環境変数として利用できる
INPUT=/home/hoge/piyo/fuga/moga/my-favorite-desk.csv
MY_SCRIPT=/home/hogehoge/piyo/fugafuga/2022-02/seller.sh
OUTPUT=/home/hoge/piyo/fuga/moga/2022-03/sales.csv
* * * 2 1-5 grep `date "+\%Y/\%-m/\%-d"` $INPUT > /dev/null || bash $MY_SCRIPT >> $OUTPUT
#...省略
        
これで随分スッキリと見やすいスケジュールにすることができました。


まとめ

今回はCronで営業日のみに定期実行されるスケジュールの実装方法について解説してみました。

祝日のみを抽出した
holidays.csvによる休日判定を定義しました。

同様のやり方を行えばカスタマイズした日付リストによるCronの特定日だけの実行も可能です。

ぜひCronでご自身の業務の自動化にトライしてみてください。

参考サイト

Cronで利用すると便利な、祝日や平日(営業日)かの判定方法

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。