洗練されたPerl: ワンライナー102

Perl 1行スクリプトの続き

今回Tedが紹介してくれるのは、指定した範囲内の行の表示からファイルの中身の逆順表示まで、Perlの簡潔な1行スクリプトを使い、わずかなコードでたくさんのことを行う方法についてです。

Teodor Zlatanov (tzz@iglou.com)Gold Software Systems

Teodor Zlatanovは1999年にボストン大学を卒業し、コンピューター・エンジニアリングで学位を取得しています。1992年以来プログラマーとして働いており、Perl、Java、C、C++などの言語を使用してきています。関心を持っている領域としてはオープン・ソース作業、Perl、テキスト構文解析、3層のクライアント/サーバー・データベース・アーキテクチャー、Unixのシステム管理などです。助言や間違いの指摘を歓迎しています。連絡先はtzz@bu.eduです。



2003年 3月 12日

本稿は、常連の読者ならおわかりだと思いますが、以前「洗練されたPerl」の連載の1本としてとりあげたワンライナー101 の続きです。今回の内容を理解するためには、絶対に前回の記事を把握しておく必要があります。以下に進む前に、前回の記事に目を通しておいてください。

本稿の目標は、前回の記事と同様、読みやすくて再利用可能なコードを示すことにあります。必ずしも、一番短いプログラムや一番効率のよいバージョンを示そうというわけではありません。このことを念頭に置いて、コードを見ていくことにしましょう。

Tom Christiansenの作品集

Tom Christiansenが何年か前にUsenetに公表した1行スクリプト集がありますが、これは今でもPerlプログラマーにとって、興味深くかつ重宝される作品集です。今回は、この作品集の中でも、複雑な内容のものを見てみたいと思います。作品集全体は、tomc.txtというファイルに収められています (このファイルをダウンロードするためのリンクは、参考文献に示してあります)。作品集は、前回のワンライナー101 の記事と少し重複しますので、共通する部分は指摘するつもりです。

Awkは、テキストをフィールドに分けていくというような基本的な作業によく使用されますが、Perlは、もともとテキスト処理に長けた言語です。というわけで、最初の一行スクリプト(ワンライナー)は、そのスクリプトへの入力テキストの2列を加え合わせるものです。

リスト1. awkに似ている ?
# add first and penultimate columns
# NOTE the equivalent awk script: 
# awk '{i = NF - 1; print $1 + $i}'
perl -lane 'print $F[0] + $F[-2]'

さて、これはどんなことを行っているのでしょうか。からくりは、スイッチにあります。-n-a の2つのスイッチは、このスクリプトを入力に対するラッパーとし、入力をホワイトスペースで配列@F に分割しています。また-e スイッチは、さらにこのラッパーに文を1つ追加しています。実際に生成される実質的なコードは、以下のとおりです。

リスト2: スクリプトの実体
while (<>) 
{
  @F = split(' ');
  print $F[0] + $F[-2]; # offset -2 means "2nd to last element of the array"
}

ファイルの中の2つの目印あるいは2つの行番号の間に含まれている部分を表示するというのも、よく行う作業です。

リスト3: 指定範囲内の行の表示
# 1. just lines 15 to 17
perl -ne 'print if 15 .. 17'
# 2. just lines NOT between line 10 and 20
perl -ne 'print unless 10 .. 20'
# 3. lines between START and END
perl -ne 'print if /^START$/ .. /^END$/'
# 4. lines NOT between START and END
perl -ne 'print unless /^START$/ .. /^END$/'

リスト3の最初の1行スクリプトには、必要な範囲がすでに処理され終わったにもかかわらず、ファイル全体 をチェックするという問題があります。3番目の例には、この問題はありません。目印START からEND までのすべての行を表示するようになっているからです。START/END の目印の組が8組ある場合、3番目の1行スクリプトは、8組全部について、その中に含まれる行を表示します。

1番目の例を非効率的なものでなくするのは簡単です。現在行を表す変数$. を使えばよいだけのことです。$. が15以上になったら表示を開始し、$. が17を超えたら終了するようにします。

リスト4: 指定した番号の範囲内の行を効率よく表示する
# just lines 15 to 17, efficiently
perl -ne 'print if $. >= 15; exit if $. >= 17;'

表示はこれぐらいにして、次に編集処理に移りましょう。言うまでもありませんが、1行スクリプトを試してみる場合、とくにデータに変更を加えるための スクリプトの場合には、バックアップをとっておいたほうがよいでしょう。1行スクリプトを少し変更しても大した違いはないだろうと思うのは、皆さんだけではないでしょう。でも、Sendmailの構成やメールボックスの編集を行うときに、そうした思い込みは禁物です。

リスト5: 即時編集
# 1. in-place edit of *.c files changing all foo to bar
perl -p -i.bak -e 's/\bfoo\b/bar/g' *.c
# 2. delete first 10 lines
perl -i.old -ne 'print unless 1 .. 10' foo.txt
# 3. change all the isolated oldvar occurrences to newvar
perl -i.old -pe 's{\boldvar\b}{newvar}g' *.[chy]
# 4. increment all numbers found in these files
perl -i.tiny -pe 's/(\d+)/ 1 + $1 /ge' file1 file2 ....
# 5. delete all but lines between START and END
perl -i.old -ne 'print unless /^START$/ .. /^END$/' foo.txt
# 6. binary edit (careful!)
perl -i.bak -pe 's/Mozilla/Slopoke/g' /usr/local/bin/netscape

どうして1 .. 10 が行番号1から10までを指定することになるのでしょうか。perldoc perlopのマニュアル・ページを調べてみてください。基本的に.. 演算子は、範囲の繰り返し処理を行う意味になります。したがって、このスクリプトは、行 を10個カウントするのではなく、-n スイッチで作成されるループの繰り返しを10回カウントすることになります (perldoc perlrun参照。リスト2にも、この種のループの例があります)。

-i スイッチのからくりは、@ARGV に指定されたそれぞれのファイルを、スクリプトの出力として生成されたバージョンで置き換えているということです。したがって、-i スイッチは、Perlをテキスト編集フィルターにします。-i スイッチといっしょにバックアップ・オプションを指定するのを忘れないで ください。i に拡張子を付けると、編集されたファイルのバックアップが、その拡張子を使って作成されます。

スイッチ-p および-n の使い方に注意してください。-n スイッチは、データの表示を明示的に指定したいときに使用します。-p スイッチは、何も指定しなくても、-n スイッチで生成されるループの中にprint $_ 文を挿入することを意味します。したがって、-p スイッチは、ファイル全体 を処理したい場合に適し、-n スイッチは、ファイルを選択的に 処理する、すなわち指定したデータだけを表示する必要がある場合に適しています。

即時編集 (in-place editing) の例は、ワンライナー101 の記事にも示されています。

ファイルの中身を逆にするというのは、あまり行われることのない処理ですが、以下の1行スクリプトは、ファイル全体を処理する場合に、-n スイッチと-p スイッチが必ずしも最善の選択ではないことを示しています。

リスト6: ファイルの中身の逆転
# 1. command-line that reverses the whole input by lines
#    (printing each line in reverse order)
perl -e 'print reverse <>' file1 file2 file3 ....
# 2. command-line that shows each line with its characters backwards
perl -nle 'print scalar reverse $_' file1 file2 file3 ....
# 3. find palindromes in the /usr/dict/words dictionary file
perl -lne '$_ = lc $_; print if $_ eq reverse' /usr/dict/words
# 4. command-line that reverses all the bytes in a file
perl -0777e 'print scalar reverse <>' f1 f2 f3 ...
# 5. command-line that reverses each paragraph in the file but prints
#    them in order
perl -00 -e 'print reverse <>' file1 file2 file3 ....

パラグラフ全体あるいはファイル全体を1個の文字列として読み出したい場合には、-0 (ゼロ) フラグが非常に便利です (この機能には、任意の文字コードを使うこともできますので、特別な文字を目印として使用することができます)。1個のコマンド (-0777) でファイル全体を読み出す場合には、大きなファイルだとメモリーを使い切ることがありますので注意が必要です。ファイルの中身を逆向きに読み出す必要がある場合 (たとえば、ログを逆から調べるような場合)、CPANにあるFile::ReadBackwardsモジュールを使うとよいでしょう。ワンライナー101 にはFile::ReadBackwardsを使ったログの解析例が示されています。

リスト6の1番目のスクリプトと2番目のスクリプトは似ているように見えますが、両方は、まったく違った働きをします。違いは、<> が (2番目のスクリプトの-n のように) スカラー・コンテキストで使われているのか (1番目のスクリプトのように) リスト・コンテキストで使われているのかにあります。

3番目のスクリプトは、回文検出器 (palindrome detector) ですが、もともとは、$_ = lc $_; というコードは入っていませんでした。Bobのような、逆順がまったく同じとは言えない回文も捕捉できるように、私が加えたものです。

私が加えた部分は、$_ = lc; と記述してもよいのですが、lc() 関数の対象を明確に示したほうが1行スクリプトがわかりやすくなると思い、このようにしました。


Paul Joslinの作品集

Paul Joslinは、この記事のために、わざわざ1行スクリプトをいくつか私のところに送ってきてくれました。

リスト7: ランダムな値を使っての書き換え
# replace string XYZ with a random number less than 611 in these files
perl -i.bak -pe "s/XYZ/int rand(611)/e" f1 f2 f3

これは、XYZ を611 (この値は、任意に選択できる) 未満の値に置き換えるフィルターです。rand() は、0から 引数までの間のランダムな値を返してくる関数でした。

置換のつどint rand(611) が評価されますので、XYZ は、毎回、異なる ランダム値に置き換えられることになります。

リスト8: ファイルの基本的な性質の開示
# 1. Run basename on contents of file
perl -pe "s@.*/@@gio" INDEX
# 2. Run dirname on contents of file
perl -pe 's@^(.*/)[^/]+@$1\n@' INDEX
# 3. Run basename on contents of file
perl -MFile::Basename -ne 'print basename $_' INDEX
# 4. Run dirname on contents of file
perl -MFile::Basename -ne 'print dirname $_' INDEX

1行スクリプト1と2はPaul作で、3と4は、Paul作のものを私がFile::Basenameモジュールを使って書き直したものです。これらの1行スクリプトの目的は単純で、システム管理者なら、これらのスクリプトを重宝するのではないでしょうか。

リスト9: 移動あるいは名前の変更。UNIXでは同じこと
# 1. write command to mv dirs XYZ_asd to Asd
# (you may have to preface each '!' with a '\' depending on your shell)
ls | perl -pe 's!([^_]+)_(.)(.*)!mv $1_$2$3\u$2\E$3!gio'
# 2. Write a shell script to move input from xyz to Xyz
ls | perl -ne 'chop; printf "mv $_ %s\n", ucfirst $_;'

慣れたユーザーやシステム管理者にとって、パターンにしたがってファイル名を変更するというのは、非常によく行う処理です。上のスクリプトは、2つの種類の仕事を行います。ファイル名の中の_ 文字までの部分を削除するか、Perlのucfirst() 関数を使って、ファイル名の1文字目が大文字になるように、ファイル名を一つ一つ変更します。

UNIXには、Vladimir Lanin作のmmvというユーティリティーがあり、これも関心のあるところかもしれません。mmvでは、簡単なパターンを使ってファイル名を変更することができ、これが、ものすごく強力な働きをします。このユーティリティーへのリンクは、参考文献に示してあります。


筆者の作品

以下は、1行スクリプトではありませんが、もともと1行スクリプトとして始まった非常に便利なスクリプトです。ある決まった文字列を置き換えるという意味ではリスト7に似ていますが、その指定された文字列に対する置換文字列そのものが、その次の指定文字列となる点が工夫されています。

このアイデアは、ずいぶん昔のニュースグループの投稿から来ているのですが、その元のバージョンを見つけ出すことができませんでした。すべてのシステム・ファイルについて、あるIPアドレスを別のアドレスに置き換える必要がある場合、たとえば、デフォルトのルーターが変わったというような場合に、このスクリプトは便利です。このスクリプトでは、書き換えたいファイルのリストに$0 (UNIXでは、通常、スクリプトの名前) を含めています。

結局、これは1行スクリプトとしては複雑になりすぎることがわかりました。また、システム・ファイルに変更を加えるときには何を行おうとしているのかを示すメッセージを表示する必要もあります。

リスト10: IPアドレスを別のアドレスに置き換える
#!/usr/bin/perl -w
use Regexp::Common qw/net/; # provides the regular expressions for IP matching
my $replacement = shift @ARGV; # get the new IP address
die "You must provide $0 with a replacement string for the IP 111.111.111.111"
 unless $replacement;
# we require that $replacement be JUST a valid IP address
die "Invalid IP address provided: [$replacement]"
 unless $replacement =~ m/^$RE{net}{IPv4}$/;
# replace the string in each file
foreach my $file ($0, qw[/etc/hosts /etc/defaultrouter /etc/ethers], @ARGV)
{
 # note that we know $replacement is a valid IP address, so this is
 # not a dangerous invocation
 my $command = "perl -p -i.bak -e 's/111.111.111.111/$replacement/g' $file";
 print "Executing [$command]\n"; system($command);
}

現在ではPerlプログラマーにとって不可欠な資源となっているRegexp::Commonモジュールを使っている点に注意してください。Regexp::Commonがなければ、たくさんの時間を費やして数字などよく使用するパターンとのマッチを手作業で記述しなければならず、間違いも冒しやすくなります。


まとめ

1行スクリプトの作品集を送っていただいたPaul Joslinに感謝いたします。1行スクリプトが触発する簡潔さの精神にしたがって、Perlの1行スクリプトについてのまとめは、ワンライナー101 を参考にしていただくことにします。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=226807
ArticleTitle=洗練されたPerl: ワンライナー102
publish-date=03122003