おでーぶでおでーぶ

いろいろ書く。いろいろ。

Cron で動かすスクリプトの開発方法例

(2015年に書いたものをコピペ)

普通の使い捨てスクリプトと同じ作り方をしてませんか?

そういう人がよくぶち当たるのが,cronで実行したらコマンドが見つからないだとか環境変数が設定されてなくてもうダメという状況.

そんな状況を回避するには以下の2つの手法を取りましょう.

  • ジョブ実行前に環境変数を設定する
  • cronでの実行環境を再現してジョブを作成する

結論だけ見る方はこちらのテンプレート

ひとまず,環境変数の確認方法

環境変数を出力するジョブを追加します.

* * * * * env >/tmp/cron_env

/tmp/cron_envの中身を見てみましょう.下記は筆者の端末(Mac)で実行した場合です.

SHELL=/bin/sh
USER=jmatsu
PATH=/usr/bin:/bin
PWD=/Users/jmatsu
SHLVL=1
HOME=/Users/jmatsu
LOGNAME=jmatsu
_=/usr/bin/env

少ないなんてレベルではありませんね. PATH に至っては正直使い物になりません.コマンドが見つからないわけです.

cronで実行するときのみ有効になるよう環境変数を設定する

なければ設定してしまえば良い,という形です. 複数の方法があり,それぞれ少しずつ特徴があります.

ログインシェルとしてシェルを起動し,それぞれのprofileを読み込む

lオプションを渡してログインシェルとしてシェルを起動させ,~/.bash_profile 等を読み込ませる方法です.

* * * * * /usr/local/bin/bash -l -c 'env > /tmp/cron_env_2'

自分馴染みの環境変数を設定することが可能になります.

ただしそのジョブごとに必要最小限の環境変数を設定するといった行為はできませんし,他人と共有するジョブで取る手段としては良いと言い切れない部分があります. 当然のことながら,lオプションでログインシェル化できないシェルは利用できません.

また読み込みに行く設定ファイルはBashだと /etc/profile, ~/.bash_profile, ~/.bash_login, ~/.profileであり,明示的に呼び出さない限り ~/.bashrc は読み込みにいきません. インタラクティブシェルとログインシェルの違いですね.

crontabに直接環境変数を設定する

こちらも各ジョブに対する設定ではなく,全体になります.

PATH=/usr/local/bin:/usr/bin:/bin
SOME_VAR="cron_env_3 and cron_env_4"

* * * * * env > /tmp/cron_env_3

SOME_VAR2="cron_env_4 only"
* * * * * env > /tmp/cron_env_4

明示的に指定することができ,影響範囲も前述の方法と違って完全にcrontab内に収まる形です. しかしながら,PATH=/usr/local/bin:$PATHPATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin と記述することはできません. まだ変数が設定されていないため・・・とかではなく,そもそもシェルスクリプトではないからです.

コマンド実行時に環境変数を明示的に指定する

こちらはシェルでお馴染みの記法です.

* * * * * PATH="/usr/local/bin:$PATH" env > /tmp/cron_env_5

各ジョブごとに設定することが可能ですし,シェルに解釈させる部分に記述できるので非常に書きやすいでしょう. 個人的にも正攻法だと思いますが,記述が長くなってくるとどうしても厳しいものがあります.

コマンド実行前に指定した設定ファイルを読み込んでおく

ジョブごとにファイルを用意すれば,柔軟に環境変数を定義しておくことが可能です.

export SOME_VAR="Hey"
# ;の次にスペースを入れると正しく動作しません.

* * * * * . ~/cron.env;env > /tmp/cron_env_6
# or
* * * * * source ~/cron.env;env > /tmp/cron_env_7

外部ファイルに必要な変数を分離しておくと非常に管理がしやすいです. 特にVCS管理下であれば,同じリポジトリ上に置いておくだけで非常に共有しやすいでしょう.

指定した外部設定ファイルを読み込んでから実行するジョブにする

上記の記法ではスペースを入れると動かないといったヒューマンエラーが避けられませんね.

ならばその自体をスクリプトとしてラップしましょう. 個人的に最良だと思っている選択肢がこれです.

* * * * * ~/run_with_env.sh > /tmp/cron_env_8
#!/bin/sh

# 外部ファイルから環境変数を読み込む
source ~/.some_profile

# 環境変数の設定を記述する
export SOME_ENV_VAR="Hey"

# job
env

cronで実行するときの環境変数を再現してデバッグする

前提

  • cronジョブ実行時の環境変数を前述した方法で取得し,/tmp/cron_env(任意)に保存しておくこと

また実行したジョブは ~/somejob.sh で実行できるとします.

直接再現する

env - $(cat /tmp/cron_env) ~/somejob.sh

として起動することで直接cronにおける環境変数状況を再現することができます.

これは最初に環境変数を空にし,

env -

次にcronジョブ実行時の環境変数を読み込むよう設定し,

env - $(cat /tmp/cron_env)

その環境変数の状態でジョブを実行することを表します.

その上で足りない環境変数は上記で紹介した いずれかの方法で設定する必要があります.

インタラクティブシェル env - $(cat /tmp/cron_env) /bin/sh 上でのデバッグやaliasの設定を行うと楽でしょう.

ただしこれはジョブを実行するステートメントで効くものであり,使い勝手が良いとは個人的には思いません.

間接的に再現する

環境変数をcronジョブ実行時と同様のものに設定し,なんらかの方法で環境変数を設定,ジョブ実行をするようなハブとなるスクリプトを用いてデバッグしましょう.

#!/bin/sh

# reset env
while read line; do
    eval "$line"
done < <(diff <(env) /tmp/cron_env|grep "^[><]"|grep -v " _="|sed -e 's/^< \([^=]*\)=.*/unset \1/' -e 's/^>/export/'|sort -r)

~/somejob.sh

環境変数リセット部分は以下の方針になります.

  1. 現在の環境変数とcron実行時の環境変数の差分を取る
  2. 差分行情報はいらないので実差分情報だけ取得する.また _ は除いておく.
  3. 差分形式を置換する.現在の環境変数をすべてunsetし,cron実行時の環境変数をすべてexportする形に整形する
  4. 先にunsetするようにソートする
  5. 整形した文字列をwhileに流し込み,1行ずつevalすることでexportあるいはunsetする.

この形式でデバッグを行うと,実行時には環境変数リセットの部分を除いた hub.sh をcronのジョブとして指定すれば良いわけですから非常に楽ですね.

直接的な再現と間接的な再現の差異

外部ファイルからの環境変数設定を考えます.

間接的に再現するケースでは,外部に環境変数用のスクリプトを用意し,source あるいは . による読み込みを行うことで実現されます.

#!/bin/sh

# reset env
while read line; do
    eval "$line"
done < <(diff <(env) /tmp/cron_env|grep "^[><]"|grep -v " _="|sed -e 's/^< \([^=]*\)=.*/unset \1/' -e 's/^>/export/'|sort -r)

source ~/.some_profile

~/somejob.sh
export EXT_VAR1="one"
export EXT_VAR2="two"

これは完全にシェルスクリプトですね.必要なように実行シェルをカスタマイズするわけです.

直接的に再現するケースでも当然のことながら不可能ではありません.

env - $(cat /tmp/cron_env) $(cat ~/ext_vars) ~/somejob.sh
EXT_VAR1="one"
EXT_VAR2="two"

しかしながら外部ファイルの中身の記述を見れば分かる通り,これはシェル変数の記述となります. つまり実行コマンドと同時に設定する方法以外では,環境変数用のファイルとしてそのまま用いることができません. まあ一手間加えて,source <(awk '$0="export " $0') のようにすればできますが.

間接的再現の仕組みの方が記法としても環境変数として直感的ですし,個人的に軍配は間接的再現側にあがると思っています.

結論

~/somejob.shというジョブを実行するという前提.

デバッグ時の環境

% ~/for_debug.sh

#!/bin/sh

# reset env
while read line; do
    eval "$line"
done < <(diff <(env) /tmp/cron_env|grep "^[><]"|grep -v " _="|sed -e 's/^< \([^=]*\)=.*/unset \1/' -e 's/^>/export/'|sort -r)

# 外部ファイルから環境変数を読み込む
source ~/.some_profile

# 環境変数の設定を記述する
export SOME_ENV_VAR="Hey"

~/somejob.sh

実行時の環境

* * * * * ~/for_runtime.sh

#!/bin/sh

# 外部ファイルから環境変数を読み込む
source ~/.some_profile

# 環境変数の設定を記述する
export SOME_ENV_VAR="Hey"

~/somejob.sh

何か間違いやアドバイス等ありましたらコメントにてお願い致します.