要約
perlワンライナーを書く上でのtipsを湯婆婆ネタの上に解説する。
はじめに
Javaを皮切りに、いろんなプログラミング言語で湯婆婆のセリフを再現してみる試みがqiita内でちょっとしたブームになっているらしい。
そんな中、
という試みを発見。
いい機会なので便乗して、perlワンライナーの書き方を再確認してみることにした。
コードはこんな感じ
まずは当該記事のワンライナーを引用させていただく。ただし視認性を考慮して;
の後ろに改行を入れてみた。
perl -le '
use Encode;
print "契約書だよ。そこに名前を書きな。";
my $n=<STDIN>;
chomp($n);
die unless length($n);
print "フーン。$nというのかい。贅沢な名だね。";
my @l=split //,decode("utf8",$n);
$n=encode("utf8",$l[int(rand(@l))]);
print "今からお前の名前は$nだ。いいかい、$nだよ。分かったら返事をするんだ、$n!!"'
さて、これを下敷きに筆者が手を加えてみたのが以下になる。239文字から190文字に縮小できたようだ(update: 2020-11-16 17:00)。
perl -CSD -Mutf8 -nle '
BEGIN{print "契約書だよ。そこに名前を書きな。"}
/^$/ and die;
my $n=substr($_,rand(length($_)),1);
print "フーン。${_}というのかい。贅沢な名だね。\n今からお前の名前は${n}だ。いいかい、${n}だよ。分かったら返事をするんだ、$n!!";
last;'
変更点について説明していきたい。
コマンドラインオプションの活用
参考:
'-CSD' でUTF-8の入出力に対応する。
-CSD
とした場合は、入出力の一切がUTF-8であると仮定され、入力されたデータは自動的にdecodeされ、出力されるデータは自動的にencodeされる。つまり、明示的にuse Encode
したりbinmode STDIN 'utf8:'
だのencode('utf8', $_)
だのはしなくてよくなる。
平たく言い換えると、pythonやら他の言語で普通に行われているように普通に日本語の読み書きができるようになるということだ。
なお、古いバージョンのperlではこのオプションは全く違う意味で使われていた。今でもその古代の情報に基づいた教科書やwebサイトが残っているかもしれないので注意してほしい。
'-Mutf8'でスクリプト内部のリテラルのutf-8化を宣言
このオプションはスクリプト冒頭にuse utf8;
を書いたのと同じ結果をもたらす。このuse utf8ほど誤解を受け混乱をきたしているプラグマは他にないだろう。歴史的経緯もあるが、世の中の解説が厳密であろうとして変に難解なものになっているのが事態を複雑化させているように見える。
ここでは厳密性をかなぐり捨てて平たく言う。このオプションはスクリプト内に全角文字を書き込んだ時にそれを正しく扱うように指示するものである。
もう少しだけ正確に言い直すと、このオプションにより、""内の文字列は自動的にdecode(内部コード化)されるようになる。この他にも変数名として半角英数字以外のいろいろな文字が使えるようになるなどの効果もあるが、ひとしく誰もが理解しておくべきことは、$a="ハロー、パール!"
なコードを書きたければuse utf8はマストであるということだ。
なお、俺は死んでも絶対にスクリプト内に漢字なんか書かないもんねという人もいるかもしれないが、そうであってもuse utf8して問題が起こることは絶対にない。だから、使い捨てでないちゃんとしたスクリプトを書く気があるのであれば、とりあえずuse strict; use warnings; use utf8;
は必ず冒頭に書いておけ。
'-n'で入出力を自動ループ化する
-n
をつけると、スクリプト全体がwhile(<>)
ループで囲われているかのように実行される。つまり、入力元から1行を読み取ってからスクリプトの内容を実行し、終わったら次の1行を読み取り、再度スクリプトの内容を実行し、これを入力が尽きるまで繰り返す。
これによって、変数=<STDIN>;
を書かなくてよくなる。
ただし、この湯婆婆スクリプトのオリジナルは、入力を一回だけ受け付けてセリフを吐かせたらそれで終了ということのようなので、それに寄せるためスクリプトの最後にlast;
を加えている。
なお、よく似たオプションに-p
がある。これは-nの挙動に加え、ループの最後でprint $_
されるようになる。少しawkに近いふるまいである。フィルタコマンド系のシンプルなワンライナーを作りたいときには便利である。
awkと同様、BEGINブロックやENDブロックがサポートされている。これにより自動付与whileループに対して前処理・後処理を付け加えることができるので、安心して-n
できるというものだ。
'-l'で改行を自動制御する
このオプションをつけると、printの実行の最後に勝手に改行してくれるようになる。また、-n
や-p
オプションを使っているときは、入力されたデータの末尾の改行コードを自動的に削除してくれる。
これによって、微妙に面倒くさい「最初にchompする」処理やprintの最後に"\n"を書き加える対処をしなくてよくなる。
コードの簡略化
近い者同士まとめる
元スクリプトでは、出力が二つに分割されていて、その間で文字列加工処理が行われていた。私のスクリプトでは、加工は加工、出力は出力でまとめた。できるだけこのように「近いことをやっているコードはまとめる」ようにしておくと、後で見返したり機能を拡張したくなったりしたときに圧倒的に楽である。
ワンライナーであればあまり問題でないが、後者のメリットは、少しややこしいツールの自作などしているときに大きい。普段からコードの順番にも気を遣う習慣づけをしておくのがいいと思う。
$_の活用
先に述べた通り、コマンドラインオプションをつけるといろいろコードを省略でき、暗黙のうちにperlがデータを変数$_
に入れたり出したり加工したりしてくれるようになる。$_
に頼りすぎるのは、長大なコードを書かざるを得ない時には危険な場合もある。しかし、ワンライナーであれば中身が簡潔であることの優先度を高く考え積極的に省略記法を使っていくのがいいだろう。
部分文字列はsubstrで取得する
元スクリプトでは対象文字列を一文字づつ切り取ってリストに入れ、しかるのちにランダムにその一つを選び出していた。私はリストへの分割をせず、substrで直接対処することにした。文字列の処理ですむ話は文字列の処理で済ませたほうが話は早い。話だけでなく実行速度も速い。対象文字列が長くなればなるほど、その差は顕著になるだろう。
BEGINする
コメントでのご指摘を受け追記。-n
や-p
をつけてスクリプトを実行した時、BEGIN{}の中に書いたコードは暗黙whileループの前に、すなわち入力を受け付ける前に実行される。本件のようにプロンプトを先に出したり、変数に初期値を与えたりするのに使える。
変数を含む文字列定数の書き方に注意
元のスクリプトでは、"フーン。$nというのかい。贅沢な名だね。"
となっていた文字列だが、改変後は$n
を${n}
のように書いている。
use utf8した代償で、ここはやや長い記法を用いざるを得ないのだ。
use utf8していない場合、変数名は「英字か下線から始まってそれに英字、下線、数字が続く文字列」とシンプルである。しかし、use utf8してしまうと、ひらがなや漢字までも変数名の一部として利用できてしまうのだ。つまり元スクリプトの内容にただuse utf8を加えただけだと、「nというのかい。贅沢な名だね。」という名前の変数を参照しようとしてしまう。
perlでは""の中に単純変数をそのまま書ける。しかし、上記のような事故を防ぐためには、bashのように${変数名}
の形式にしなければならない。use utf8しない場合でもこの形式に統一しておいた方が安全である。アンダースコアを地の文の中に含む文字列を""の中に書きたいこともあるだろう。
${変数名}
で変数の内容を参照する記法は""の中だけで通用する。""の外では全く違う意味となる。すなわち「変数のデリファレンス」と解釈されるので、混同しないようにしよう。
おわりに
さらっと修正ネタを投稿するつもりが、いざ解説に取り掛かってみるとかなりの分量になり、公式サイトを読み直したりテストスクリプトを書いたり、なかなかオオゴトになってしまった。いつもながらperlの世界の奥深さを思い知らされる。
しめくくりに、テストスクリプトの実行結果を示すので、まあクスっと笑っていただければ幸いである。
$ cat yubaba1.sh
#!/bin/sh
a=ドナルド・J・トランプ
echo test A
echo ${a}| \
perl -le 'use Encode;print "契約書だよ。そこに名前を書きな。";my $n=<STDIN>;chomp($n);die unless length($n);print "フーン。$nというのかい。贅沢な名だね。";my @l=split //,decode("utf8",$n);$n=encode("utf8",$l[int(rand(@l))]);print "今からお前の名前は$nだ。いいかい、$nだよ。分かったら返事をするんだ、$n!!"'
echo ''
echo test B
echo ${a}| \
perl -CSD -Mutf8 -nle 'BEGIN{print "契約書だよ。そこに名前を書きな。"}/^$/ and die; my $n=substr($_,rand(length($_)),1);print "フーン。${_}というのかい。贅沢な名だね。\n今からお前の名前は${n}だ。いいかい、${n}だよ。分かったら返事をするんだ、${n}!!"; last;'
$ sh yubaba1.sh
test A
契約書だよ。そこに名前を書きな。
フーン。ドナルド・J・トランプというのかい。贅沢な名だね。
今からお前の名前はドだ。いいかい、ドだよ。分かったら返事をするんだ、ド!!
test B
契約書だよ。そこに名前を書きな。
フーン。ドナルド・J・トランプというのかい。贅沢な名だね。
今からお前の名前はJだ。いいかい、Jだよ。分かったら返事をするんだ、J!!