Wednesday, October 15, 2008

即興スクリプティング

「こういうことしたいんですけど、どうすればいいですかねえ」という質問を受けた。 "こういうこと"というのは次のようなことだった。
  • あるテキストの入力ファイルがあり、中身は次のようなかんじ
    Mapping JP {
      ...
    }
    Mapping AU {
      ...
    }
    Mapping JP {
      ...
    }
    
  • Mapping XX {という行ではじまり}でおわるのが一つのブロック。
  • この中で Mapping JP { ... }というブロックをすべて抜きだしたい
「ああ、それならsedで簡単にできますよ。」
こんなかんじで
 % sed -ne '/^Mapping JP {/,/^}/p' datafile
  • defaultでは出力しない(-nオプション)
  • Mapping JP {ではじまる行から、}ではじまる行までは出力する(/start/,/end/ という範囲で pコマンド)

しばらくして…
「実はフォーマットがちょっと違ってました。これならどうなりますか?」
  • テキストの入力ファイルがあり、中身は次のようなかんじ
    Mapping {
     id: foo
     country: "JP"
     ...
    }
    Mapping {
     id: bar
     country: "AU"
     ...
    }
    Mapping {
     id: baz
     country: "JP"
      ...
    }
    
  • Mapping {という行ではじまり}でおわるのが一つのブロック。
  • この中で country: "JP"を含むブロックをすべて抜きだしたい
「うーん、sedだとちょっと面倒だなあ。hold spaceかな」
といっていたら
perlでmultiline regexp使ったら簡単じゃないですか?」
で、でてきたのがこれ
% perl -n0e 'print "$_\n" for /^Mapping {[^}]+country: "JP"[^}]+}/gms'\
   datafile
  • sedawkのようにループして処理する(-nオプション)
  • record separetorとしてnull文字を設定する(-0オプション)。つまりファイルを全部一気に読む
  • ファイル全体に対して^Mapping {[^}]+"JP"[^}]+}にマッチする部分をそれぞれ出力する。gでglobal matching、mでmultilineとして処理する(^は文字列の先頭にマッチするのでは行頭にマッチするようになる)、sでsinglelineとして処理する(.なども改行文字にもマッチするようになる)
ちなみにsedだとこう
% sed -ne '/^Mapping {/,/^}/H;/^}/{s///;x;/country: "JP"/p}' datafile
  • defaultで出力しない(-nオプション)
  • Mapping {ではじまる行から}ではじまる行までhold spaceにためていく。(/start/,/end/ という範囲で Hコマンド)
  • }ではじまる行で次の処理を行う({...}で処理するコマンドをグルーピング
  • s///でまず現在の行(})を消す
  • hold spaceをpattern spaceに戻す(xコマンド)
  • 戻してきたpattern spaceでcountry: "JP"があれば出力(pコマンド)
最初s///するのを忘れていて、無駄な}がでちゃうなあというあたりでちょっとはまっていた。でもperl版より短い。

ちなみに世の中にはsgrepとかいうのがあって、次のように使えるっぽい
% sgrep 'outer("Mapping {" .. "}" containing ("country: \"JP\""))'\
   datafile
  • Mapping {から}の中にcountry: "JP"が含まれていたらそのブロック全体を出力
まあ、わかりやすいように見えるけどこのsyntaxを覚えとく価値はあるんかなあ。