Perl Hackers Hub

第64回少しマニアックなPerlのテクニック―特殊変数、低レベルの標準関数を使いこなす(1)

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはkarupaneruraこと佐藤健太さんで、テーマは「少しマニアックなPerlのテクニック」です。

本稿のサンプルコードは、WEB+DB PRESS Vol.119のサポートサイトから入手できます。すべてのコードは本誌執筆時点(2020年9月)で広く使われている最新版のUbuntu 18.04.5LTSおよびPerl 5.32.0で動作確認しています。

短くシンプルにコードを書き上げるテクニック

みなさんもご存じのとおり、CPANモジュールを使えば高度な処理を簡単に行えます。一方で、サーバでスクリプトを実行したい場合は、サーバの実行環境のperl処理系を使うことになります。

このとき、各サーバにCPANモジュールを新しくインストールするのは難しい場面が多いでしょう。対象環境のCPANモジュールを不用意にアップグレードすると、思わぬ影響が出るかもしれません。また、CPANモジュールの中にはインストール時にCのライブラリとリンクを行うものもあるため、簡単にそれが使えるとも限りません。特に古いサーバに対しては容易ではないでしょう。

そんな場面においては、コンパイル型言語を用いて静的リンクで実行バイナリをビルドしたものを配布できますが、この方法はコンパイルを必要とします。そのため、サーバで実際のデータを読んで動かして調整する必要がある用途では、少々面倒です。

そこで今回は、Perlに慣れている人にもそうでない人にも役立つ、CPANモジュールに頼らずにPerlの基本的な機能を上手に活用するテクニックを紹介します。

まず(1)では、その場で短くシンプルにコードを書き上げるテクニックを解説します。

特殊変数$_を使う

Perlの特色の一つとして、特殊変数$_の存在が挙げられるでしょう。この特殊変数の役割は大きく分けて2つあります。一つは、たとえばforeachのループ値の変数の指定を省略した際に$_へ自動的にループ値が代入されるような、その一連の処理における現在の値を示す役割です。もう一つは、ucなどの組込み関数のデフォルトの引数としての役割です。

$_は暗黙的に使われるものであるため、$_をやみくもに使うと、処理の対象が明示的ではないわかりにくいコードになりがちです。また、ネストしたコードで$_を使うと、どれがどの$_なのか見分けが付きにくく、間違いの温床になります。

しかし、裏を返せば、自分自身しか使わないコードで場面を選んで使う場合は、短いコードで目的を達成できる便利な道具であるとも言えます。また、mapgrepなど処理する対象が自明である場合にも便利です。$_の使いどころを考えるヒントを表1にまとめました。

表1 $_の使いどころを考えるヒント
検討事項使ってもよい場面使うべきでない場面
必要な事前処理少ない多い
扱うデータの種類少ない多い
処理の内容単純複雑
処理の対象自明自明ではない

ワンライナーで使う

$_を便利に活用できる代表的なユースケースとして、ワンライナーが挙げられるでしょう。ワンライナーとは、コマンドライン引数としてプログラムそのものとなるコードを渡して実行するスタイルを指します。

次のコードは、Perlのワンライナーで書いたHello,World!です。

$ perl -e 'print "Hello, World!\n"'

ちょっとした問題を手早く片付ける際に、ワンライナーは便利です。特に、grepsedなどのUNIXコマンドだけでは複雑になる場面でも、Perlのワンライナーであればシンプルに書ける場合があります。

そして、ワンライナーにおいて$_は非常に便利に使えます。たとえば、標準入力をすべて大文字にして標準出力に出力するワンライナーは次のように書けます。

$ perl -pe '$_=uc'

処理系perl-pオプションを使用することで、このコードは次のように展開されます。

B::Deparseを用いて展開したコード

LINE: while (defined($_ = readline ARGV)) {
    $_ = uc $_;
}
continue {
    die "-p destination: $!\n" unless print $_;
}

引数に指定したコードがループの中に展開されます。これは、標準入力または引数に指定したファイルの各行ごとにループして、continueセクションでprintするしくみです。そして、指定したコードのucの引数が、デフォルト引数である$_に補完されています。

別の例として、sedの代替としてperlを使って、正規表現による置換も行えます。

$ perl -pe 's/foo/bar/g'

この例も、パターンマッチ演算子である=~を利用せずに正規表現による置換を行うため、デフォルトの$_が処理の対象になります。結果として、sedのような処理をこれだけで実現できます。

このように、暗黙的に対象を示す変数とそれを支援するためのコマンドラインオプションがあるため、Perlのワンライナーはシンプルに書けます。

ちなみに、処理系としてのperlにはほかにも-n-aなどさまざまなコマンドラインオプションが実装されています。それぞれのオプションが問題にはまれば、本質的な部分の記述だけで問題を解決できます。

foreachと組み合わせて使う

一般的にforeachはループを書くときに使われますが、$_を代入するためだけに使うこともできます。なお、Perlにおいてforeachforと等価ですので、以後はforとして説明します。

次の例では、$fizzbuzz_textがfizzかbuzzを含む場合に文字列を出力します。

say "$fizzbuzz_text includes fizz"
    if $fizzbuzz_text =~ /fizz/;
say "$fizzbuzz_text includes buzz"
    if $fizzbuzz_text =~ /buzz/;

十分わかりやすいですが、$fizzbuzz_textという名前は長く見通しが悪いです。かといって、これを短くすれば意味のわからない命名になりかねません。

forを使って書きなおすと、次のようになります。

for ($fizzbuzz_text) {
    say "$_ includes fizz" if /fizz/;
    say "$_ includes buzz" if /buzz/;
}

forは通常リストに対してループを行いますが、この場合はスカラ変数を指定しているためループ回数が1回のループとなります。また、ループ変数を指定していないため、ループ変数として$_が使われます。結果的にこのforのブロックは、$_$fizzbuzz_textとして扱うブロックとして使えます。そして、先ほどのワンライナーの例と同様に=~演算子を利用せずに正規表現マッチを行っているため、デフォルトの$_がその処理の対象になります。

しくみは少し複雑ですが、イメージさえ理解していればうまく扱えるでしょう。

入出力のフォーマットをコントロールする

Perlはテキスト処理に特化して作られた歴史から、どのようなフォーマットで入出力するのかをある程度コントロールできます。Perlには、入出力のコントロールを行うための特殊変数がいくつか存在します。これらの特性を理解して適材適所で正しく活用すると、シンプルで効率の良いコードが書けます。

入力の行セパレータを変更する

最もよく使われるものは、入力の行セパレータを意味する特殊変数$/です。Perlには、<>演算子や組込み関数のreadlineなど、行単位で入力を扱う機能が存在します。この特殊変数はその行の単位を定めるための区切り文字となります。この特殊変数の値は環境によってデフォルト値が異なっており、たとえば一般的なLinux環境ではLF\nに設定されています。

この特殊変数の値を変更すれば、行セパレータを変更できます。たとえば、Linux環境でCRLF\r\nのファイルをPerlから読み込みたい場合は、$/\r\nを設定して読み込むことで自然と処理できます。

純粋に入力の行セパレータを変更する目的でも$/を利用できますが、最も身近な例はファイルハンドルからすべての内容を読み込む処理でしょう。この処理は、CPANモジュールのFile::SlurpPath::Tinyslurpメソッドなどがその実装として存在するように、Perlにおいてはslurpと呼ばれるのが一般的です。

slurp$/を使わずに素朴に実装すると、次のようになります。

my $all_of_texts = '';
for my $line (<$fh>) {
    $all_of_texts .= $line;
}

$/を使えば、これをより簡単に実装できます。

$/ = undef;
my $all_of_texts = <$fh>;

$/undefとするとPerlはファイルの終端までを1行として読み込みます。これによって、slurp相当の処理を簡単に実現できます。

なお、このような特殊変数の変更は、その影響範囲を限定するために、localを使ったダイナミックスコープで局所化して行うことが一般的です。

my $all_of_texts = do {
    # 初期化せず局所化するとundefになる
    local $/;
    <$fh>;
};

ほかの特殊変数を扱う場合でも、必要に応じてlocalで局所化して扱うとよいでしょう。

出力の行・列セパレータを変更する

出力においても、入力の場合と同様にセパレータを変更できます。Perlにおいて出力のための基本的なインタフェースはprintです。デフォルトでは出力のセパレータは規定されず、printを連続して呼び出してもprintの引数の内容が単に続けて出力されます。

# "helloworld"と出力される
print "hello";
print "world";

printは一般的には1つの引数の例しか示されませんが、実は複数の引数を出力できます。

# "helloworld"と出力される
print "hello", "world";

これも同様にデフォルトのセパレータは規定されず、つながって出力されます。

ここでは便宜上、printの呼び出しごとに末尾に付くセパレータを行セパレータ、printの引数の間に付くセパレータを列セパレータとして説明します。

行セパレータを変更するためには、特殊変数$\を使います。使い方は先ほど説明した$/と同様です。

local $\ = "\n";

# "hello\nworld\n"と出力される
print "hello";
print "world";

列セパレータを変更するためには、特殊変数$,を使います。これも使い方は同様です。

これらを組み合わせると、CSVComma-SeparatedValuesカンマ区切り)を簡単に出力できます。

local $, = ",";
local $\ = "\n";

print "date", "message";
print "08/22", "too hot!";

これは次のように出力されます。

date,message
08/22,too hot!

素朴に実装するにはjoinなどを使う必要がありますが、これを使えばシンプルな実装になります。$,\tにすれば、TSVTab-Separated Valuesタブ区切り)も出力できます。

配列を文字列に展開する際のセパレータを変更する

出力の列セパレータとなる特殊変数$,に似ていますが、配列を文字列に展開する際のセパレータとなる特殊変数$"も存在します。これは、配列を文字列の中で展開する際に、その要素のセパレータになります。デフォルトでは空白文字になっています。

my @arr = qw/foo bar baz/;

# "foo bar baz"と出力される
print "@arr\n";

これもさまざまな用途がありますが、リストを読みやすく整形する用途が主でしょう。以下はエラーメッセージを生成する例です。

my @result = doit(); # 何かしらの処理
if (is_fail(@result)) { # 結果から失敗を判定
    local $" = ', ';
    die "failed. result: @result";
}

この例では、たとえば@resultが("a", "b", "c")となり、かつis_failが真になるとき、failed. result: a, b, cというメッセージで例外を発生させます。

<続きの(2)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    ブロックチェーン、スマートコントラクト、NFT
    作って学ぶWeb3

おすすめ記事

記事・ニュース一覧