環境に依存しないワンライナーを書くならsedよりperlの方がいい

  • 152
    Like
  • 0
    Comment
More than 1 year has passed since last update.

まえがき

sedで環境に依存しないワンライナーを書きたかったけど、BSDとGNUの実装で微妙に挙動が違うせいで難しかった。で、いっそのことperlで書いた方がいいのでは、という結論になった。ぐぐったらperlのワンライナーの書き方出てくるけど何でそう動くのか分からなかったのでそれも調べた。そこらへんについてまとめる。

GNU sedとBSD sedの違い、perlからsedへの乗り換え方簡易版、その詳細、という感じでまとめたので知りたいところからどうぞ。

sedにはGNU sedとBSD sedがある

sedで

$ echo 'hoge' | sed -E 's/(.*)g/\1/'

とかしようとすると実行する環境によって失敗する。

sedにはBSDの実装とGNUの実装があるからだ。-EはBSD sedで拡張正規表現を使うオプションで、GNU sedで拡張正規表現を使いたければ-rを使う必要がある。

じゃあ拡張正規表現を使わずにデフォルトの正規表現を使えばいいのでは?という話になるが、デフォルトの正規表現にもBSD sedとGNU sedで微妙な違いが存在する。sedのデフォルトの正規表現はbasic regular expression(以下BRE)と呼ばれるものだが、このBREには更に素のBREとenhanced BREというものが存在し、BSD sedでは素のBREでGNUはenhanced BREの模様1。具体的に何が違うかというと、enhanced BREでは\+, \?, \|がメタキャラクタとして使えるが、素のBREでは使えない2\+はBREでは\{1,\}, \?\{0,1\}と変換できるが、残念なことに\|はBREで表現することが出来ない。ていうかそもそもBREはobsoleteらしい3

更に悪いことに、改行周りがBSDは残念な感じになっている。
例えば$PATHを見やすくするため、:を改行に変えたいとする。
GNU sedだと

$ echo $PATH | sed 's/:/\n/g'
/usr/local/bin
/usr/bin
/bin
/usr/local/games
/usr/games

こう書けるのに、BSD sedだと:nに変換される。

$ echo $PATH | sed 's/:/\n/g'
/usr/local/binn/usr/binn/binn/usr/local/gamesn/usr/games

同様のことをするにはこうする必要がある4

$ echo $PATH | sed 's/:/\'$'\n/g'
/usr/local/bin
/usr/bin
/bin
/usr/local/games
/usr/games

改行のところ呪文にしか見えない。
あるいは、バックスラッシュに続けて改行を入れる。

$ echo $PATH | sed 's/:/\
/g'
/usr/local/bin
/usr/bin
/bin
/usr/local/games
/usr/games

もはやワンライナーではない。
いちいちこれらの互換性を考えてsedのスクリプトを書きたくない5

そこでperlですよ

perlならsedのBSDとGNUのような違いを気にする必要はない。マイナーバージョンは違うかもしれないがメジャーバージョンはperl5を期待していい6

だけど、文字列置換とかに特化してるsedに比べてもっと汎用的な目的で作られたperlだと記述が煩雑になるのでは?学習コストも高く付きそう。

と思っていた時期が僕にもありました。

調べてみると、sedとほぼ同じ感覚で書けることが分かった。

例えばsedでこういう置換をする場合

$ echo -e "hoge\nfuga\npiyo" | sed -e 's/p/P/g; s/\(.*g\).*/\1/g;'
hog
fug
Piyo

perlでこう書く

$ echo -e "hoge\nfuga\npiyo" | perl -pe 's/p/P/g; s/(.*g).*/$1/g;'
hog
fug
Piyo

違いは

  • sedがperlに(当たり前
  • オプションに-p追加
  • 正規表現がBREからperlのものに(ここでは具体的には\(\)()になってる)
  • 一致した文字列を取り出すのが\1から$1

これだけ。この例だとパイプで入力を受け取っているが、sed同様ファイル名を渡して実行することも可能。

これだけでsedのBSDとGNUの違いを考えなくてよくなるなら安いもんだと思う。その上正規表現がperlのになって表現力が上がるし、perlを覚えればもっと複雑なことをしやすくなる。

ただ、筆者がsedを文字列置換にしか使ったことがないので、他の機能を使おうとするとどうなるか知らない。

perlのワンライナーの詳細

単純に文字列置換するのにsedからperlに乗り換えるのは上記のことを知っていれば十分だと思う。が、これだけだと実現できないちょっと違うことをしたくなった人や、そもそもなんでその記述でsedみたいに動くのか?ということが知りたい人のために、もう少し詳細を書く。

まず、-eオプションは引数に取る文字列をperlのコードとして実行するオプション。
次に、-pオプションはperlをsedっぽくするオプション。
例えばperl -pe 's/o/O/g'を実行する場合、以下のようなプログラムを実行するのと同じになる

while (<>) {
    s/o/O/g
} continue {
    print or die "-p destination: $!\n";
}

<>は、perlのコマンドライン引数にファイル名を渡していたらそこから1行ずつ読み出し、引数がなかったら標準入力から1行ずつ読み出すイテレータ。
で、perlには$_というデフォルトの引数に割り当てられた特殊変数があり、これは色んな所で省略出来る。
というわけで、上記のコードを省略せずに書くとこうなる。

while (defined($_ = <>)) {
    $_ =~ s/o/O/g
} continue {
    print $_ or die "-p destination: $!\n";
}

つまり、標準入力orファイルから1行ずつ読み出し$_に格納し、s/o/O/g$_を書き換えて、$_printする、という挙動になる。
結果、perl -pe 's/o/O/g'と書くとsed -e 's/o/O/g'と同じ挙動になる、というわけ。

細かい話だが、上記の例だとセミコロンをつけていない。これはperlはブロック内の最後のステートメントのセミコロンを省略出来る、という性質のため。上記のwhileブロックの中で実行される最後のステートメントはセミコロンを書かなくて良い。また、-eは複数繋げられる。複数つないだ場合、それぞれが1行ずつのステートメントだと解釈される。よってこの場合最後の-eの最後のステートメントのみセミコロンを省略できる。

発展

普通の文字列置換するときの仕組みの理解は上記のことを知っていれば十分だと思う。更に複雑なことをしたい時の話をする。

-pオプションの補足

-pをつけた場合、awkのようなBEGIN, ENDが使える。つまり、例えば最後に行数を表示したかったらこんな感じになる。

$ echo -e "hoge\nfuga\npiyo" | perl -pe 'BEGIN{$count=0} s/o/O/g; $count++; END{print $count, "\n"}'
hOge
fuga
piyO
3

awk同様変数は数値として扱う場合0で初期化されるので、この例の場合BEGINは省略できる。

$ echo -e "hoge\nfuga\npiyo" | perl -pe 's/o/O/g; $count++; END{print $count, "\n"}'
hOge
fuga
piyO
3

また、-lオプションをつけるとprintした時に勝手に改行を入れてくれる。

$ echo -e "hoge\nfuga\npiyo" | perl -ple 's/o/O/g; $count++; END{print $count}'
hOge
fuga
piyO
3

-nオプションで必要な行だけ出力 or 出力しない

必要なところだけ出力したい or そもそも出力したくない、という場合には-pではなく-nを使う。-nを使うと、以下のコードのように実行される。

while (<>) {
...             # your program goes here
}

-pオプションの時と比べてcontinueブロックがなくなっただけ。代わりに出力したいときは自前でprintする必要がある。例えば、先頭がfで始まる行のみ出力、とかだとこうする。

$ echo -e "hoge\nfuga\npiyo" | perl -ne 'print if /^f/;'
fuga

-pオプション同様、BEGIN, ENDが使えるので、例えば行数だけ数えて表示したい場合はこうなる

$ echo -e "hoge\nfuga\npiyo" | perl -nle '$count++; END{print $count}'
3

-aでawkっぽい処理

また、-aawkみたいに何列目の文字列だけ出力、というのが出来る。awkの場合$1のようにn番目の列を指定するが、perlの-aオプションの場合@Fという配列に格納される。よって、awkでの$1はperlでは$F[0]になる(数字がずれるので注意)。その行全体を意味する変数はawkでは$0で、perlでは(今までどおり、省略されがちな)$_

例えば、awkでこういうのは

$ echo "a1 b1 c1\na2 b2 c2" | awk '{print $2}'
b1
b2

perlではこうなる

$ echo "a1 b1 c1\na2 b2 c2" | perl -alne '{print $F[1]}'
b1
b2

これは具体的にはこういう処理が内部で走ってると思えばいい

while (<>) {
    @F = split(' ');
    ...             # your program goes here
}

また、awk同様-Fでセパレータを変更できる。セパレータは正規表現で指定も出来る。

$ echo "a1, b1, c1\na2, b2, c2" | perl -F'[0-9]' -alne '{print $F[1]}'
, b
, b

現在の行番号

$.が行番号になる。

$ echo "a\nb\nc\nd\ne\nf" | perl -nle 'print "$.:$_"'
1:a
2:b
3:c
4:d
5:e
6:f

空行区切り

-00オプションで空行区切り(paragraph mode)になる。

$ echo 'hoge\nfuga\n\npiyo\n\nbar\nbaz' | perl -00 -nle 'print if /^fuga$/m'
hoge
fuga

まとめ

sedが環境によって挙動が違うこと、perlとsedの文字列置換は似てるので乗り換えやすいこと、perlのワンライナーの詳細をまとめた。
色々書いたけど、なんだかんだ言ってperlにオプション-pつけたら大体sedっぽく動くと思っといて、複雑なことしたかったら他のコマンドとパイプで組み合わせてやるのが一番コスパ高いと思う。オプション覚えるの大変だし7。で、余裕が出てきたらそろそろもうちょっとperlによせてやってみようかな、という感じでストレスにならない程度にやるのが良さそう。
あと、実際にperlに乗り換えて使ってみたわけではなくて、調べた段階でこのエントリにまとめてるので、実際にやってみるとやっぱsedの方がいいわ、とかあるかもしれない。
perl詳しくない人が書いたので間違ってたらツッコミ歓迎。

参考


  1. ドキュメントに書いてあったわけじゃなく、試しに動かした感じそうだった、というだけなので確実にそうなってるとは言えないけど。 

  2. 違いはこれだけじゃない。\sあたりのメタキャラクタ使えなかったりとか。詳細はBSDのman 7 re_formatENHANCED FEATURESの項目を参照。 

  3. これもman 7 re_format参照。obsoleteと書いてるとは言え、本当に使えなくなることはもはやないと思う。 

  4. http://nlfiedler.github.io/2010/12/05/newlines-in-sed-on-mac.html 参照 

  5. BSD sedの改行の例はGNU sedでも動くんだけどね。 

  6. ラリー・ウォールが2015年のクリスマスにperl6を出すって言ってるらしいので、10年後とかはデフォルトperl6かもしれないけど。 

  7. 筆者はこのエントリ書いてるうちに覚えちゃったけど