開眼シェルスクリプト2013年12月号

Pocket
LINEで送る

出典: 技術評論社SoftwareDesign

24. 開眼シェルスクリプト 第24回 cron/crontabとシェルスクリプトを組み合わせる

 皆様、定時出勤、定時退社してますか?
などと最初から挑発ともとれるような発言で炎上しそうですが、
今回の開眼シェルスクリプトは定時に何かをマシンにさせるcron、
具体的にはcrontabの使い方を扱います。
どうか、右手にお持ちの日本刀を鞘に戻してください。

 cronは、実行したい日時を指定しておけば、
プログラムを自動実行してくれる仕組みです。
Macを含むUNIX系のOSでは、ほとんどの場合、
最初から動いており、
crontab というコマンドを使って、
定期的に動作させたいプログラムを指定するだけで
プログラムが動くようになっています。

 本連載では2012年の10月号で使った例があります。
このときはさらっと説明しましたが、
cronには少し癖みたいなものがあるので、
落とし穴に嵌るとなかなか思うようになりません。
この癖は、シェルスクリプトを間に挟む事で緩和できます。
今回はほんのさわりだけですが、基本的な方法を示します。

 ところで、定期実行というと、昔、
カントというオッサンがいて、あまりに時間に忠実で、
カントが散歩で家の前を通る時刻に
合わせて家の人が時計を合わせたなどという逸話が残ってます。
cronも、それくらいは正確です(当たり前)。
ついでにこのオッサンの言い放ったことを書いておきます。

我が行いを見習えと、誰にでも言い得るよう行為せよ。
— イマヌエル・カント

先生、無理です。

 ところで、話が逸れたついでに報告しますと、
本連載は今回で終了です。当初、ネタが地味なだけに
6回連載して様子を見るという話でしたが、
シェルスクリプト=UNIXという筆者の勝手な拡大政策によって
気づけば今回で24回目です。
現在の風潮である、
みんなで一生懸命に莫迦でかいブラックボックスの使い方を勉強し、
みんなで愛と情熱を以てブラックボックスの
バージョンアップ地獄をフォローしていくという、
おおよそ工学的アプローチとは思えない騒ぎからのコペルニクス的転回を狙い、
今後も寒風の中裸一貫、地味にシェルスクリプト活動をしていく所存です。
あ、コペルニクス的転回も、カントの言った事です。
うまくまとまりました。

24.1. cronとcrondとcrontabの関係

 さて、まず用語の整理から。
自分でインストールしなくても、だいたいの環境においては
cronという仕組みが動いています。
cronは仕組みの名前であって、実際には、
crond というサービスが最初から後ろで動いています。
crondというのは、cronのデーモンという意味ですね。
ただ、どの環境でもcrondの名前が crond
であるとは限らず、リスト1のように cron
になっていたりします。ややこしや。

  • リスト1: 環境によって crond が動いていたり、 cron が動いていたり。
1
2
3
4
5
6
[ueda@CentOS ~]$ ps aux | grep cron | grep -v grep
root      1397  0.0  0.0  20408   452 ?        Ss   Feb04   7:25 crond
uedamac:OSX ueda$ ps aux | grep cron | grep -v grep
root           10058   0.0  0.0  2432784    156   ??  Ss   日03PM   0:00.39 /usr/sbin/cron
ueda@Ubuntu:~$ ps aux | grep cron | grep -v grep
root       748  0.0  0.0  19112   848 ?        Ss   Sep18   0:01 cron

  crontab は、cronで定期実行するプログラムを確認したり、
設定したりするためのコマンドです。
crontab で設定した内容はどこかにファイルで保存されているのですが、
このコマンドを通している限りはどこにあるか気にしなくて構いません。
あるユーザで crontab を呼び出し、
実行内容を書き込むと、その実行内容は、
そのユーザで動作します。
そして crontab に書いた自動実行のリストは、
crond を再起動しなくてもすぐに有効になります。
いつもcronを使っているような人でも、
crond を再起動したという経験は少ないかと思います。
筆者もありません。

24.2. crontabの使い方

crontab の使い方ですが、まずは設定内容の編集方法から説明します。

$ crontab -e

と打つと、エディタ(ViかVim)が立ち上がります。
(Macの場合は注意があるので後述します。)

例えばここに次のように打ってみましょう。
(脚注:Viが使えないと苦労しますが、
その場合は vimtutor という練習用コマンドがありますので、
まずはそっちを練習しましょう。)

* * * * * touch /tmp/aaaa

これで普通のViの操作で保存して終了します。
ちゃんと登録されているかどうかは、
crontab -l で確認できます。

$ crontab -l
* * * * * touch /tmp/aaaa

1分くらい待って /tmp/aaaa
ができたらうまくcronが働いています。
さらに、1分ごとに ls を打ってみると、
タイムスタンプが変化している様子が分かります。

uedamac:~ ueda$ ls -l /tmp/aaaa
-rw-r--r-- 1 ueda wheel 0  9 22 16:24 /tmp/aaaa
uedamac:~ ueda$ ls -l /tmp/aaaa
-rw-r--r-- 1 ueda wheel 0  9 22 16:25 /tmp/aaaa

なぜそうなるかは後から説明しますが、
cronが1分ごとに touch
を起動して /tmp/aaaa のタイムスタンプを更新しているからです。

 今度は設定を消してみます。
crontab -e で編集してもよいのですが、
crontab -r とやると、設定が全部消えます。

uedamac:~ ueda$ crontab -r
uedamac:~ ueda$ crontab -l
crontab: no crontab for ueda

これは「 crontab -r で消しましょう」と言うよりは、
crontab -r を押すと大変な事になるぞ!」
という注意の意味で紹介しました。

24.2.1. ファイルでcrontabの内容を管理

 さて、 crontab はMacでも使えますが、
crontab -e で編集した内容が反映されないという現象が発生します。
どうもVimと相性が悪いようです。
(脚注:http://d.hatena.ne.jp/yuyarin/20100225/1267084794
http://d.hatena.ne.jp/shunsuk/20120122/1327239513
等で調査しました。)
Vimの設定ファイルをいじると解決するようですが、
ここではもうちょっと確実な方法を示しておきます。

 まず、名前はなんでもよいので、
以下のようなファイルを自分で作ります。
筆者はホームの下に etc を掘ってその下に、
crontab.conf という名前で作りました。

uedamac:etc ueda$ cat crontab.conf
* * * * * touch /tmp/aaaa

 次に、 crontab にこのファイルを読み込ませます。

uedamac:etc ueda$ crontab crontab.conf

-l オプションで確認しましょう。

uedamac:etc ueda$ crontab -l
* * * * * touch /tmp/aaaa

こうやれば、 crontab
からViを呼び出したときに起こる不具合とは無縁です。
また、設定してやらなくても好きなエディタを使えます。

 また、これを応用すると、リスト2のような事もできます。

  • リスト2: crontabでリストを出し入れ
1
2
3
4
# crontabの内容を書き出す
uedamac:etc ueda$ crontab -l > hoge
# crontabに書き出した内容を戻す
uedamac:etc ueda$ crontab hoge

この例は同じ物を書き出したり読み出したりしているだけで全く意味がないのですが、
別のサーバや別のユーザに、
cronの設定を簡単に移す事ができます。
そして、 crontab -r をやらかしても、
またファイルを読ませれば復旧できます。

 ただし、この方法には欠点が一つあって、
crontab.conf を書いて満足してしまい、
読み込ませることを忘れがちになります。
ご注意を。

24.2.2. コマンドの前の記号の意味

 さて、次に時刻の指定の方法を説明します。
書式のマニュアルは man 5 crontab で調べることができますので、
ここでは最小限の説明をします。

 先ほど crontab で指定した

* * * * * touch /tmp/aaaa

ですが、この * * * * * の部分が時刻の指定部分です。
順番に、分・時・日・月・曜日の指定で、
* はそれぞれ毎分、毎時、毎日・・・ということになります。
つまりはワイルドカードです。
上の例では、毎分、 touch /tmp/aaaa を行うという意味になります。
最小単位が分なので、最小の周期は1分ということになります。

 時刻の指定の例を一気に示します。
例えば、(1)毎時5分に実行したい、(2)5分ごとに実行したい、
(3)毎時15分と30分に実行したい、(4)月曜日の14時〜20時まで、
毎時30分に実行したい、というのを上から順に示すと、
リスト3のようになります。曜日の数字は、日曜から土曜まで、
0から6で指定します。日曜は7と書いてもOKです。
結局、次の点を押さえて慣れるということです。

  • リスト3: crontabの書き方あれこれ
1
2
3
4
5
uedamac:etc ueda$ crontab -l
5 * * * * touch /tmp/aaaa
*/5 * * * * touch /tmp/bbbb
15,30 * * * * touch /tmp/cccc
30 14-20 * * 1 touch /tmp/dddd
  • スラッシュの後ろに数字を書くと、その数字の周期で実行
  • カンマで数字を並べると、その数字に該当する時に実行
  • ハイフンで数字をつなぐと、その範囲内で毎回実行

ハイフンについては、n-mと書いたら、nとmも含まれます。
また、ハイフンとスラッシュの併用もできます。
あとのことは、使いながら解説します。

24.3. Twitterへの自動ツイートにcronを使う

 さて、cronを使って何か作ってみましょう。
今回もMacで試します。リスト4に環境を示します。

  • リスト4: 実験環境
1
2
uedamac:~ ueda$ uname -a
Darwin uedamac.local 12.5.0 Darwin Kernel Version 12.5.0: Mon Jul 29 16:33:49 PDT 2013; root:xnu-2050.48.11~1/RELEASE_X86_64 x86_64

 それで、cronを使って何をしようかといろいろ考えたのですが、
今回はTwitterで自動ツイートを行うプログラムをしましょう。
と言っても一からシェルスクリプトでbotを作ると大変なので、
出来合いのコマンドを使います。
また、筆者が試した環境は OS X Server ではなく、
MacBook Air なので、
サスペンド状態だったりネットワークに接続されていなかったりすると、
ツイートできません。
ただ、今回の内容を他のUNIX環境に移植するのは簡単です。

24.3.1. ツイートコマンドの準備

https://github.com/ryuichiueda/TomoTool/blob/master/Twitter/usptomo-tweet
に、筆者が作ったつぶやきコマンド(シェルスクリプト)
usptomo-tweet をダウンロードします。
ダウンロードの方法がわからなかったら、
画面をコピペして usptomo-tweet ファイルに保存して、

$ chmod +x usptomo-tweet

としてください。コマンド名が長いので、
別に tw と変更しても構いません。

  usptomo-tweet 内部ではいろいろなコマンドを使っています。
ほとんど標準的なものですが、
nkf, curl, openssl コマンドあたりは
インストールされているか確認ください。
所詮書きなぐりのシェルスクリプトなので、
何か動かなかったら自分でログみて直すくらいの気持ちでお願いします。

 次に、鍵やトークンというものの設定を行います。
https://dev.twitter.com に行って、
ツイートしたいアカウントでログインします。
ログインしたら「My applications」の画面、
「Create an application」の画面に進み、
必要事項を入力してください。
アプリケーション名は何でも大丈夫です。
必要事項の入力後、登録のボタンを押すと、
「Consumer key、Consumer secret、
Access token、Access token secret」が取得できます。
普通に取得すると、Consumer keyもAccess tokenも、「Read only」
になっているはずです。画面の指示に従って「Read and write」
というアクセスレベルで再取得してください。
ここら辺、ややこしいのですが、
説明し出すと長くなってしまうので、
うまくWeb上で方法を見つけながらやってみてください。

 取得できたら、リスト5のようなファイルをホームの下に置きます。
これは usptomo-tweet に読み込ませるシェルスクリプトの一部なので、
シェルスクリプトの文法で書き、ファイル名も間違えないようにします。
もしホーム下に置くのがいやだったら、
usptomo-tweet の中を書き換えます。

  • リスト5: キーとトークンを書いたファイル
1
2
3
4
5
uedamac:~ ueda$ cat twitter.key
CONSUMER_KEY="aaaaaaaaaaaaaaaaaaaaaa"
CONSUMER_SECRET="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
ACCESS_TOKEN="000000000-cccccccccccccccccccccccccccccccccccccccc"
ACCESS_TOKEN_SECRET="ddddddddddddddddddddddddddddddddddddddddddd"

24.3.2. ツイートしてみる

 これで準備OKです。テストしてみましょう。
リスト6のように打ってみます。

  • リスト6: 端末からテストツイート
1
uedamac:~ ueda$ ./bin/usptomo-tweet 'test: 東東東南南南西西西北北北白白'

投稿がうまくいけば、図1のようにネット上にツイートが放出されます。

  • 図1: 投稿される

24.3.3. cronから使う(環境変数に気をつけて)

 さて、cronと usptomo-tweet を組み合わせて使ってみましょう。
とは言うものの、
cron には慣れていても慣れていなくてもいろいろな落とし穴があって、
なかなか一発でうまくいきません。
粘り強くいきましょう。
ここでは一つだけ、よく起こるミスを、
デバッグしながら紹介します。

 まず、リスト7のように仕掛けてみましょう。
時刻は直近のものに合わせます。
数分余裕を持って仕掛けるようにと書いてあるサイトがありますが、
おそらく余裕を持たせなくても大丈夫な環境がほとんどだと考えます。

  • リスト7: crontabにコマンドを直接書き込む
1
2
uedamac:etc ueda$ crontab -l
21 21 * * * /Users/ueda/bin/usptomo-tweet 'test: びろーん' > /dev/null 2> /tmp/error

時刻がきたら、 /tmp/error を見てみましょう。
環境にもよりますが、筆者のMacではリスト8のように失敗しました。

  • リスト8: nkfが見つからないエラーが発生
1
2
3
4
uedamac:etc ueda$ less /tmp/error
(略)
/Users/ueda/bin/usptomo-tweet: line 33: nkf: command not found
(略)

あれ? nkf がインストールされていないのかな?
というところですが、
先ほど端末から試したときにはうまくいっていたので、
ここはパス(環境変数 PATH )を疑います。

  crontab でリスト9のように仕掛けます。
ついでに LANG も調べてみましょう。

  • リスト9: echoでcronで設定されている環境変数を調べる
uedamac:etc ueda$ crontab -l
23 10 * * * echo "$PATH" > /tmp/path
23 10 * * * echo "$LANG" > /tmp/lang

時刻が来たら /tmp/path を見てみると、
リスト10のようになっていました。

  • リスト10: 環境変数の調査結果
uedamac:etc ueda$ cat /tmp/path
/usr/bin:/bin
uedamac:etc ueda$ cat /tmp/lang

(LANGには何も入っていない)

nkf の場所は次のように /usr/local/bin/
なので、 nkf が見つからずエラーが起きたようです。

uedamac:etc ueda$ which nkf
/usr/local/bin/nkf

 cronで何かを動かそうとしてうまくいかない場合、
大抵はパスが間違っているか、
今の例のように環境変数が端末で使っている
ものと違うという問題に突き当たります。

 さて、原因が分かったので対策を。
まず、リスト11のようにcrontabに環境変数を設定する方法があります。

  • リスト11: 環境変数をcrontabで指定する方法
1
2
3
4
uedamac:etc ueda$ crontab -l
PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
MAILTO=""
35 10 * * * /Users/ueda/bin/usptomo-tweet 'test: びろーんぐ' > /dev/null 2> /tmp/error

 結果は掲載しませんが、これはうまくいきます。
ただ、こうやって全体を見渡すとごちゃごちゃしていますし、
usptomo-tweet のために通したパスが他の設定にも影響します。
筆者はあまり良い方向に行っているとは思えません。

 ついでに書いた MAILTO="" は、
cronがログのメールを送ってくるのを防ぐための記述です。

24.3.4. ラッパーのシェルスクリプトを使う

 ここでシェルスクリプトの出番です。
環境変数やその他を全てシェルスクリプトの中に押し込んでしまいます。
ラッパーのシェルスクリプトは、
ホーム下に batch というディレクトリを掘って、
そこに置く事にします。
リスト12に、例をお見せします。
これは、Macを閉じてしまうとツイートできないのを逆手にとって、
夜にMacを開いていたら自虐ツイートする仕組みです。

  • リスト12: cronから呼び出すシェルスクリプトと設定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
uedamac:batch ueda$ cat nightwork
#!/bin/bash -xv

PATH=/usr/local/bin:/Users/ueda/bin:$PATH
LANG=ja_JP.UTF-8

exec 2> /tmp/stderr.$(basename $0)
exec > /tmp/stdout.$(basename $0)

usptomo-tweet '[自動ツイート]上田さん、こんな時間になってもまだPC開いて仕事をしてるんだって〜。キャハハダッサイ!'

// 実行できるようにしましょう
uedamac:batch ueda$ chmod +x nightwork
// crontabは次のようにセット
uedamac:batch ueda$ crontab -l
MAILTO=""
30 23 * * * /Users/ueda/batch/nightwork

PATHには、 nkf のある /usr/local/bin と、
usptomo-tweet のある /Users/ueda/bin を指定します。
また、 exec 2> でこのシェルスクリプトの標準エラー出力、
exec > で標準出力をリダイレクトしてファイルに残しておきます。
basename $0 は、このシェルスクリプトの名前( nightwork
になります。

 図2のようにちゃんと送信されました・・・。

  • 図2: 送信の確認

 悲しいですね。もう寝ることにします。

24.4. おわりに

 今回はcronとシェルスクリプトと組み合わせて、
自動自虐ツイートを行う自動送信機能を MacBook Air
に組み込みました。
シェルスクリプトという点では、
最後の最後にちょっと出てきただけでしたが、
PATHの明示的な指定など、
これまでの連載で説明できなかったことを扱えました。
最後に作った nightwork
を拡張していくと、例えばブログの記事を紹介したり、
リストからmongonをランダムに選んでつぶやく
ボットを作ったりすることができます。
ぜひ試していただければ。

 冒頭でお伝えした通り、
開眼シェルスクリプトは今回で最終回です。
ご愛読、ありがとうございました。

Pocket
LINEで送る