crontabでPerlスクリプトを動かそうとしてハマった話

crontabとは

以下がわかりやすいです。

www.server-memo.net

@songmuさんによる以下のシリーズも充実しています。
gihyo.jp

そのcrontabでPerlスクリプトを動かそうと思ったところ、以下のようなハマり方をしたのでメモ。

なお、環境はMacです。

crontabはシステムPerlで動く

最初にテスト的なコードを書いてみたらあっさり動いたので、逆に気づくまで時間がかかってしまったのだけど、たとえば以下のようなPerlのコードをホームディレクトリに置いておいて、

#!/usr/bin/env perl
#
# hello.pl

use strict;
use warnings;
use feature 'say';

say "Hello!";

crontabをこのように設定してみると、

10 * * * * perl hello.pl >> test.txt

普通にtest.txtの中に

Hello!

が書き込まれていく。(この場合は毎時10分に「Hello!」が追記されていく)

ただし、このときにhello.plやtest.txtをホームディレクトリ以外の場所に置くなら、それは絶対パスで記述しなければならない。

10 * * * * perl $HOME/sample/hello.pl >> test.txt

とか、

10 * * * * perl /Users/note103/sample/hello.pl >> test.txt

とか。

そこまではまあ、そうだろうなっていう感じなんだけど、どうもタスクが増えるにつれて、その辺を正確に記述してもうまく動かないケースが出てきた。

具体的には、ターミナルで普通に実行するぶんには動くんだけど、crontabに設定すると動かないとか。

で、先に結論というか原因から言うと、上で何気なく記述している「perl」というコマンドが、普段ぼくが使っている「perl」とは別物だった。

ぼくは普段、plenvというのを使って、Perlの現状ほぼ最新バージョンである 5.24.0 を使っているんだけど、crontabはそのplenvとかは経由せずに、マシンにもともと入っているシステムperlを使っていて、ぼくが使っているMacのそれは5.18.2だった。*1

システムPerlに必要なモジュールが入ってない

で、しかしこれは単にバージョンが違うというだけの話ではなくて、システムPerlの方は何しろ普段は使っていないから、いつも使っている要インストールな非標準モジュール(厳密には5.18.2時点で標準モジュールではないそれ)が入ってない。

だから当然のことながら、そういうモジュールを使っているスクリプトは動いてくれなくて、けっこうハマった。

その後、これについては動作する/しない条件を手を替え品を替え特定していく中で、「うわ、これもしかしてシステムPerlで動いてるのか」と気付き、さらに「システムPerlにはあのモジュールが入ってないのか……」などと気付いていったので、とりあえずはシステムPerlにcpanmをインストールして(それも入ってなかった)、以後はcpanm経由で必要なモジュールをどんどんインストールして、なんとか動くようになった。

システムPerl以外のPerlを指定できない

それですべて解決かと思ったんだけど、どうも一個だけ、そこまで設定してもターミナルからの実行とcrontabでの実行とで動作の異なるものが残っていた。

具体的には、以下のようなコード。(実際にはちょっと違うけど、最小限の再現コード)

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use LWP::UserAgent;

my $url   = "https://pawoo.net";
my $res   = LWP::UserAgent->new->get($url);
my $title = $res->title() // 'No title';
say $title;

これを普通にターミナルから(v5.24.0で)実行すると、こうなるんだけど、

Mastodon (マストドン) hosted by pixiv - Pawoo

crontabから(v5.18.2で)実行すると、こうなる。

No title

ようは後者だとLWP::UserAgentが望みどおりの挙動をしてくれてないということで、でもモジュール自体はシステムの方にも入っていることを確認したので、そうなると原因はモジュールの有無ではなく、Perl自体のバージョンが古いこと以外に思いつかない。

というか、それ以外の原因があるとしても、次に打てる対策は「とりあえずシステムPerlのバージョンを上げても同じ結果になるかを確認する」ということ以外に考えられないということ。

なんだけど、システムPerlのバージョンを上げるというのはちょっと聞いたことがなくて、実際Perl入学式の諸先輩に聞いても「普通そういうことはしない」という話だったので、そこでまただいぶハマった。

で、今サラッと書いたように、本件についてはここで一旦完全に行き止まりという感じになったので、Perl入学式のチャットルームで例のごとく先輩方に相談してみたところ、いろいろな対処法を教えて頂いた。

たとえば、先に.bashrcを読み込ませて、普段使っているplenvなりperlbrewなりを使うように設定するとか。
あるいは、ラッパースクリプトを介してplenvなどを噛むようにしておくとか。

で、そういった案の中に「絶対パスperlを指定する」という話もあって、これはすぐ試せそうだったのでやってみたら、動いた。

具体的には、ぼくの環境だと以下のような感じ。

10 * * * * /Users/note103/.plenv/versions/5.24.0/bin/perl /Users/note103/sample/hello.pl >> test.txt

って、これだとちょっと可読性が低いので、実際には変数を利用してこんな感じで。

perl524="/Users/note103/.plenv/versions/5.24.0/bin/perl"
pathtodir="/Users/note103/sample"

10 * * * * $perl524 $pathtodir/hello.pl >> test.txt

この変数の使い方などについては、冒頭にも挙げましたが @songmu さんの以下の記事が参考になりました。
http://gihyo.jp/dev/serial/01/perl-hackers-hub/002502

で、その記事はまた上記のチャット相談会で @papixさんから教えてもらったのだったけど、その他にも短い時間でいろんな方がそれぞれの知見を提供してくださって、先輩方には本当に感謝です。

コード内に相対パスが残っていた

というわけで、長かったcrontab問題もようやく解決……としばらく安心していたのだけど、まだ終わっていなかった。

というのも、自作モジュールを使ったコードの中で、以下のようにlibモジュールをuseしていたのだけど、

use lib './lib'

これが相対パスだったので、やはりcrontabの方では動いていなかった。

なので、これを絶対パスにしなきゃ……と思って直し始めたのだけど、念のためこのlibモジュールについて、こちらもいつもお世話になっています木本さんの以下の解説を読んでいたら、
d.hatena.ne.jp

use FindBin;
use lib "$FindBin::Bin/lib";

とした方がスマートに見えたので、それに入れ替えたらそれでも動くようになった。
多謝!

ということで、一連のメモはひとまず以上なのだけど、上記で踏み抜いた穴のいくつかについては、以下のWebページでも簡潔に解説されていたので、あわせて参考情報として貼っておきます。

プロンプトとcronでの実行結果の違い − Linux Square − @IT

そのまえに、よくありがちなパターンを書いておきますと、

というのがあります。2番目が結構厄介で、僕もその為に何回か書き直したことがあります。

この後者の話なんて、最後に挙げたものそのものだった。

*1:この辺の記述、「Perl」と「perl」で表記ユレしているように見えるが、一応言語名としては大文字始まりの「Perl」、コマンド名としては全部小文字で「perl」としている。ちゃんと調べていないのでその使い分けがいいのかどうかはわからないが……。