めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Perl Tips for Quick Hack.

「Perl で取り急ぎXXXやれないかなぁ。。。」という際に使える Tips を徒然に書いてみます。みなさんにお勧めするというよりも「私はこうやっている」という紹介 + 自分のための覚え書きです。ディストリビューション依存はあまりないと思いますが、一応、RHEL5 前提です。思いついたら追加していきます。

既存の取り急ぎ・・・系文書はこちらです。

※ Perl Native でできる処理をコマンド外だしするのはいかがなものかと・・というご指摘をいただいたので、Pure Perl バージョンのコードを並記することにしました。(2011/03/30)

コーディングスタイル

書き捨てのコードだからこそコーディングスタイルはしっかり守ってつまらないバグが混入しないようにしたい。私のテンプレートはこんな感じ。(コーディングスタイルには常に恣意性があります。あくまで私のやり方です。)

#!/usr/bin/perl

use strict;      # お約束
use warnings;    # お約束
my $Logger  = "logger -t HOGEHOGE -p local6.info";  
my $Tmpfile = "/tmp/hogetmp$$";
# 大域変数は最初にまとめて宣言。頭は大文字。テンポラリファイルは PID ($$) で修飾。

# サブルーチンはここに記載
sub hoghoge {
my ( $hogahog, $hogihogi );   # ローカル(lexical)変数は最初にまとめて宣言。
my @horihori;                 # サブルーチンの途中で my は書かない。
                              # 冒頭の my 宣言群はあえてインデントしない。

}

# Main ルーチンは最後に MAIN ラベルで括って記載
MAIN: {
my $hogehoge;

}
# vi:ts=4

冒頭の $Logger はログ出力用です。安易な一時ログファイルを作るとあとでゴミになるので、きちんと syslog 経由で出すようにします。syslog.conf でファイルを分けます。

/etc/syslog.conf

*.info;mail.none;authpriv.none;cron.none;local6.none            /var/log/messages
local6.*                                                        /your_log_file

※ Logger コマンドではなく Perl の機能で syslog を吐く場合は Sys::Syslog を使います。

use Sys::Syslog;

MAIN: {
    openlog( "HOGE", "pid", "local6" );

    syslog( "LOG_INFO", "asdf" );

    closelog();
}

MAIN の頭とお尻で openlog(), closelog() を呼んでおいて、syslog を吐きたいところで syslog() を使います。

シェルコマンドの実行

system コマンドでシェルコマンドを実行する際は、ちょっとした Wrapper を使って実行結果を残すとよいでしょう。先の $Logger を使って、実行するコマンドとその出力結果をログに出力する関数です。

sub logcmd {
my ( $cmd, $noexec ) = @_;
my $res;
    system ( "$Logger -- \"$cmd\"" );
    unless ( $noexec ) {
        $res = `$cmd`; 
        system ( "echo \"$res\" | $Logger" );
    }
    return $res;
}

単なるログメッセージを出す関数としても流用できるように、第二引数に適当な値(False 以外)を入れると、コマンド名をログに出すだけで実行は行わないようにちょっと工夫してあります。

    logcmd ( "--- Restarting HTTPD ---", "noexec" );  # ログメッセージ
    logcmd ( "service httpd restart" );               # コマンド実行

実行結果のエラーチェックはしていません。経験的に、この手のちょっとしたお仕事系スクリプトでは、ぐだぐだエラーチェックを入れるよりは、エラーチェックが不要なぐらいに自明な処理しかさせない方がよいです。

※ コードインジェクションに関するコメントを頂いたのでこれも補足しておきます。外部入力などの正体の分からないものを logcmd() に渡すのはもちろん危険です。教科書的には、「正体の分からないものを渡すときは、きちんとサニタイズしてから渡しましょう。(そもそも、logcmd にサニタイズ処理いれろ??)」なんですが、「とりあえず系」のスクリプトでは、そもそもサニタイズが必要な処理はしない方が得策です。

コマンド引数の受け取り

ついつい $ARGV[ 0 ] とか使いそうになりますが、ぐっとこらえて、Getopt::Std を使いましょう。

use vars qw( $opt_h $opt_a $opt_b $opt_c );
use Getopt::Std;

sub chkopts {
    getopts( 'ha:b:c:' );

    $opt_h = 1 unless ( <必須の引数が足りない場合> );
    if ( $opt_h ) {
        print <<EOF;

usage: this_script.pl [-abc]
   -a: hoge
   -b: hogehoge
   -c: hogehogehoge

EOF
        exit 0;
    }
}

出力ファイルにタイムスタンプを付ける

TIMTOWTDI。私はこれ。

my $Logfile = "your_log_file_" . `date +%Y%m%d_%H%M%S`; chomp $Logfile;

※ Pure Perl 版はこちら。

use POSIX qw(strftime);

my $Logfile = "your_log_file_" . strftime( "%Y%m%d_%H%M%S", localtime() );

実行結果の行単位処理

ちょっとしたコマンドならこんな感じで。

    $res = logcmd ( "ls /data" );
    for $line ( split ( "\n", $res ) ) {
        $line =~ m/.....
    }

壮大な出力(もしくは時間がかかる出力)ならパイプで受けるのがよいと。

    open( IN, "soudaina_command|" );
    while ( <IN> ) {
        chomp $_;
        $_ =~ m/....
    }
    close IN;

※ 「ちょっとしたコマンド」の例で "ls /data" はよくなかったかもです。ディレクトリ内のファイル一覧がほしいのであれば、もちろん普通はこうします。

#!/usr/bin/perl

while (</data/*>) {
        print $_ . "\n";
}

ファイルハンドラの扱い

知っている人は知っている通り、open で作成されるファイルハンドラはグローバル変数になります。Perl 5.6 以降はローカル(lexical)変数にハンドラを入れることもできますが、私は、あえてグローバル変数として扱います。複数のサブルーチンで共通のファイルに出力する場合は、MAIN の頭で open して MAIN 最後に close します。

ファイルのオープン/クローズは全体像が直感で把握できるレベルに留めるべきで、ちょっとしたお仕事系スクリプトで想定外にハンドラ名がバッティングするような心配がでてきたら、そもそも全体の構造を見直したほうがよい。

メールを送る(受信はしなくていい)

DNS 登録されたグローバル IP を持ったサーバなら、とりあえず sendmail を起動すれば送れます。/etc/resolv.conf はちゃんと書いて下さい。プライベートネットワーク内にいて、外部のメール・ゲートウェイを通す場合は、SMART_HOST を利用。

/etc/mail/sendmail.mc

define(`SMART_HOST',`<Mail Gateway の FQDN>')dnl

スクリプト内で mail を実行するときは、本文は、テンポラリファイルに書き出しておいて、iso-2022-jp に変換しつつ送信。
あやしいサーバの root からのメールを送らないように $MailFrom で普通に受信可能なメールアドレスを指定するのがよいです。Subjetct は英語でいいでしょう。

my $MailTo = 'hoge@hogehoge,hoga@hogahoga';
my $MailFrom = 'ore@oreore';
    logcmd( "iconv -f utf8 -t iso-2022-jp $MailTmp | mail -s "$Subject" $MailTo -- -f $MailFrom"

構造化された Conf ファイルを読む

ちょっと取り急ぎ系ではない気もしますが、「配列のハッシュのハッシュ」みたいたなデータ構造が宣言なしにずばっと使えるのは、Perl らしい所。構造化された Conf ファイルを読み込む時などにこんな感じで使えます。

my %Config;

sub read_conf {
my ( $target, $message, $action );
my $check_config = $_[ 0 ];

    open ( IN, "<$Conf_file" );
    while (<IN>) {
        chomp; $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_=~ s/#.*$//;
        next unless ( $_ );
        if ( $_ =~ m/^:(.+)/ )     { $target  = $1; next; }
        if ( $_ =~ m/^(\(.+\))$/ ) { $message = $1; next; }
        $action  = $_;
        next unless ( $target && $message );
        push ( @{$Config{ $target }->{ $message }}, $action );   # ずばっ!!!!
    }
    close IN;

    return unless $check_config;

    print "Config file: $Conf_file\n";
    foreach $target ( keys %Config ) {
        print "\nLogfile: $target\n";
        foreach $message ( keys %{$Config{$target}} ) {
            print "  Message: $message\n";
            foreach $action ( @{$Config{ $target }->{ $message }} ) {
                print "    Action: $action\n";
            }
        }
    }
    print "\n";
    exit 0;
}

なお、配列を foreach で要素別に処理する時は、配列要素の意図しない上書きに要注意。

foreach $s ( @hoge ) {
    $s = "puyo";           # @hoge の要素が書き換わる!
}

X日以上前のファイルを消す

これ。

logcmd ( "find /data -daystart -maxdepth 1 -name \"hogehoge_*.tar.gz\" -mtime +$Days -exec rm {} \\;" );

※ うーん。これの Pure Perl 版は・・・「取り急ぎ」では書けないかも。

Oneliner

IP アドレスを取得するなど。

SERVER_IP=`ifconfig eth0 | perl -ne 'print $1 if m|addr:(\d+\.\d+\.\d+\.\d+)|'`