M.Hiroi's Home Page

Linux Programming

お気楽 Perl プログラミング超入門

[ PrevPage | Perl | NextPage ]

Perl の便利な機能

今回はまだ説明していない Perl の便利な機能を紹介します。

●ヒアドキュメント

ヒアドキュメント (here-document) とは UNIX 系のシェルにある機能で、<< と次に書かれた記号 (終端記号) を指定すると、次の行から終端記号までの複数行をひとつの文字列として扱います。簡単な例を示しましょう。

リスト : ヒアドキュメント (1)

use strict;
use warnings;

print <<EOF;
hello, world
EOF
$ perl sample2101.pl
hello, world

画面には hello, world と表示されます。<< と EOF の間には空白を入れてはいけません。空白は空の識別子とみなされ、最初に現れる空行までが文字列として扱われます。また、終端記号だけが現れる行までを文字列として扱うので、終端記号の前後に空白を入れてはいけません。

<< の後ろに定義する終端記号は、" や ' や ` で囲むことができます。" で囲まれた場合はヒアドキュメント中で変数展開が行われます。なにも囲まない場合も同じです。' で囲んだ場合は変数展開は行われません。これは文字列の場合と同じですね。` で囲んだ場合は、各行をコマンドとして実行し、その実行結果 (コマンドが標準出力へ出力したデータ) がドキュメントの内容となります。

たとえば、ディレクトリの内容を取り込みたい場合は、次のようにすればいいでしょう。

リスト : ヒアドキュメント (2)

use strict;
use warnings;

print <<`EOF`;
ls *.txt
EOF

これで拡張子が txt のファイル情報を取り込むことができます。

●フォーマット

フォーマットとは、文字列や数値を整形して出力する機能のことです。プログラムの中で format 文によりフォーマットを宣言しておくと、関数 write を呼び出すことで整形された出力を得ることができます。Perl の write はC言語の関数 fwrite や write とはまったく違い、フォーマット専用の出力関数なので注意してください。また、Perl には関数 read がありますが、これは write の逆操作ではなく、C言語の関数 fread や read と同様の動作を行います。

フォーマットは次のように宣言します。

format 名前 =
フォーマットリスト
.

名前を省略すると STDOUT になります。フォーマット名は同じ名前のファイルハンドルに対応し、整形結果はそのファイルハンドルに出力されます。もちろん Perl のことですから、ファイルハンドルとフォーマットの対応を変更することもできます。それから、最後のピリオド ( . ) をお忘れなく。

フォーマットリストは、次の 3 種類があります。

  1. コメント行
    # から始まる行はコメントとして扱われる。
  2. ピクチャー行
    出力される 1 行文の書式を指定する。
  3. 引数行
    ピクチャー行のフィールドに埋めこむ値を指定する。

ピクチャー行は、値が埋め込まれるフィールドを除けば、書いた文字がそのまま出力されます。関数 printf の書式文字列と似ていますが、フィールドの指定方法はとてもわかりやすいものです。フィールドの指定は @ か ^ で始まります。通常は @ を使いますが、詰め込み整形を行いたい場合は ^ を使います。

フィールドの指定には、次の 4 種類があります。

@##.###    数値
@||||||    センタリング
@<<<<<<    左寄せ
@>>>>>>    右寄せ

フィールドの幅は、@ から指定の終わりまでとなります。データがフィールドに収まらない場合は切り捨てられます。フィールドは、1 行に複数指定することができます。この場合、引数行では値をカンマ ( , ) で区切ります。

●ヘッダの出力

フォーマットはページの先頭に付けるヘッダにも対応しています。ファイルハンドルに _TOP を付けた名前のフォーマットは、各ページの先頭で起動され、ヘッダを出力することができます。

簡単な例題として、テキストファイルを 1 行 60 桁で折り返して出力するプログラムを作ってみましょう。このとき、ファイル名とページ数をヘッダに出力することにします。プログラムは次のようになります。

リスト : 60 桁で折り返す

use strict;
use warnings;

my $filename = shift;

format STDOUT_TOP =

File @<<<<<<<<<<<<<                               Page @||||
     $filename,                                        $%
------------------------------------------------------------

.

format STDOUT =
^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ~~
$_
.

open(IN, "$filename") or die("file open error\n");
while(<IN>){
  write;
}
close(IN);

プログラムの内容は簡単ですね。STDOUT へ出力するので、ヘッダ用のフォーマット名は STDOUT_TOP となります。ここでファイル名とページ数を表示します。現在のページ数は特殊変数 $% に格納されています。このほかにも次のような変数が定義されています。

$~    フォーマット名
$^    ヘッダ用フォーマット名
$=    1ページ当たりの行数
$-    現在のページに残されている行数
$|    0 以外の値の場合、print や write を実行するたびにフラッシュする

これらの変数はファイルハンドルごとに用意されているので、値を変更する場合は select を使うか、FileHandle モジュールを使って変数にアクセスします。

select は print や write がデフォルトで出力するファイルハンドルを設定します。そして、選択されたファイルハンドルの特殊変数にアクセスすることができます。たとえば、ファイルハンドル OUT にほかのフォーマットを設定してみましょう。

$save = select(OUT);
$~ = "MY_FORM";
$^ = "MY_FORM_TOP";
select($save);

select の返り値は、今まで設定されていたファイルハンドルです。select でファイルハンドルを変更したままにしておくと、print や write が期待した動作をしない、つまり、標準出力へ書き出すデータがファイルハンドル OUT へ出力される、ということが起きるのです。そこで、元のファイルハンドルを変数 $save に格納しておいて、フォーマット用の特殊変数を変更したら、保存しておいたファイルハンドルに戻しておきます。これで、ファイルハンドル OUT に対応するフォーマットは MY_FORM となり、ヘッダは MY_FORM_TOP となります。

なお Perl 5 の場合、select を使うよりも FileHandle モジュールを使う方が簡単なようです。興味のある方は調べてみてください。

●pack と unpack

次は pack と unpack によるバイナリデータの操作方法を説明します。もともと Perl はテキスト処理に便利なように設計されているため、バイナリデータを単純に操作することはできません。

ところで、テキストとバイナリの違いをご存じでしょうか。ファイルも大きく分けると、「テキスト」と「バイナリ」の 2 種類があります。最初に、テキストとバイナリの違いから説明します。

●テキストモードとバイナリモード

テキストファイルは、今読んでいるこのドキュメントのように、アスキーコードや utf-8 などが格納されていて、cat コマンドなどで画面に表示することができるファイルのことです。バイナリファイルは、perl などの実行ファイルや画像データである jpeg ファイルなど、テキストファイル以外のことを指します。バイナリファイルはテキストファイルと違い、アスキーコードや utf-8 以外のコードが含まれているため、cat コマンドで画面に表示すると、わけのわからない文字を画面にまきちらすことになります。

たとえば、数値 1 を print でファイルに書き込んでみましょう。この場合、ファイルに書き込まれるデータは 1 を表すアスキーコード 0x31 となります。1 そのもの (0x01) を書き込む形式がバイナリファイルとなります。Windows でファイルをオープンする場合、この 2 つの違いに注意してください。テキストファイルであればテキストモードで、バイナリファイルであればバイナリモードでファイルをオープンします。

このように説明すると、2 つのアクセスモードが存在することは、とても自然なことのように思えます。ところが、Unix 系の OS ではテキストモードは存在せず、すべてのファイルをバイナリモードで扱うことができるのです。実は Microsoft 社の OS (MS-DOS, Windows など) の仕様が、この 2 つのモードを生み出したのです。

当たり前のことですが、テキストファイルには「行」がありますね。この行を表すデータが改行文字 '\n' (0x0a) です。たとえば、「あいうえお」の「い」と「う」の間に '\n' を挿入してみましょう。

$ perl -e 'print "あい\nうえお\n";'
あい
うえお

「あい」が 1 行に表示され、「うえお」が次の行に表示されます。

ところで、改行文字 '\n' までを 1 行とする仕様は、Unix 系 OS での話です。そして、Unix という OS を開発するために設計されたプログラム言語が「C言語」です。当然のように、C言語も '\n' までを 1 行として扱います。その後、C言語は Unix から一人歩きを始め、他の OS のプログラミングにも使われるようになりました。

このとき、「行」の取り扱い方が問題 [*1] になったのです。というのも、Microsoft 社の OS (MS-DOS や Windows など) では、'\r\n' (0x0d, 0x0a) の 2 バイトで改行を表していたからです。したがって、"あいうえお\n" と出力しても、Windows では 1 行とはみなされません。

この違いを吸収するために考えられた方法が、「テキストモード」と「バイナリモード」なのです。次に示すように、テキストモードでは改行コードの変換が行われます。

読み込み時 : '\r\n' -> '\n'
書き込み時 : '\n'   -> '\r\n'

バイナリモードの場合は、改行コードの変換は行われません。今まで説明したファイルのアクセスは、すべてテキストモードで行われています。Windows でも '\n' だけで改行できたのは、陰で改行コードの変換が行われていたからなのです。

C言語の場合、ファイルをオープンするときにバイナリモードを指定するのですが、Perl はファイルをオープンしたあと、関数 binmode でバイナリモードに切り替えます。

binmode ファイルハンドル

binmode はファイルをオープンしてから、そのファイルハンドルに対して実際の入出力操作を行う前に実行してください。一度バイナリモードに切り替えたら、そのファイルハンドルをテキストモードに戻すことはできません。もっとも、ファイルにアクセスする場合、途中でテキストモードとバイナリモードを切り替えることはありません。

簡単な例を示しましょう。

リスト : テキストモードとバイナリモード (sample2103.pl)

use strict;
use warnings;

open my $out1, ">test21.txt";
print $out1 "abc\ndef\nefg\n";
close $out1;

open my $out2, ">test21.bin";
binmode $out2;
print $out2 "abc\ndef\nefg\n";
close $out2;

ファイル test21.txt をライトオープンし、"abc\ndef\nefg\n" を書き込みます。この場合はテキストモードなので、Windows で実行すると改行コードの変換により 0D, 0A が書き込まれます。ファイル test21.bin はファイルをオープンしたあと、binmode でバイナリモードに切り替えています。この場合、改行コードの変換は行われないので 0A だけが書き込まれます。

このプログラムを Ubunts で実行すると、test21.txt と test21.bin の中身は同じになります。

$ perl sample2103.pl
$ cat test21.txt
abc
def
efg
$ cat test21.bin
abc
def
efg
$ cmp test21.txt test21.bin
$

バイナリデータを格納したファイルを扱う場合、このような変換作用があるとデータを破壊してしまいますね。テキスト以外のファイルを Windows で扱う場合は、バイナリモードでなければいけません。

-- note --------
[*1] このほかにも、ファイル終了コード '^Z' (0x1a) の取り扱い方も問題になりました。 MS-DOS や Windows では、ファイルの途中に ^Z が現れると、その時点でファイル終了と見なします。

●バイナリデータの操作

Perl の場合、バイナリデータを操作するには pack と unpack を使います。たとえば、整数をバイナリ形式でファイルに格納するには、pack でバイナリ形式の文字列 (バイト列) に変換してから print でファイルに書き込みます。逆に、ファイルから読み出す場合は、read と unpack を使います。

pack template, list => byte_string
unpack template, byte_string => list

pack は第 1 引数で指定したテンプレート文字列に従い、第 2 引数以降のリストをバイト列に変換します。テンプレート文字列は型指定文字を並べたもので、繰り返しを指定する数値やアスタリスク * を付けることができます。unpack は pack と逆の動作を行います。第 1 引数には pack と同様にテンプレート文字列を与え、第 2 引数にバイナリデータを含む文字列を与えます。unpack はデータを展開してリストに格納して返します。

それでは、使用できる型指定文字を簡単に説明していきましょう。最初は整数値の変換です。

c  符号付き char 値
C  符号なし char 値
s  符号付き short 値
S  符号なし short 値
i  符号付き int 値
I  符号なし int 値
l  符号付き long 値
L  符号なし long 値
q  符号付き long long 値
Q  符号なし long long 値

char, short, int, long, long long の大きさを以下に示します。

char       1 byte
short      2 byte
int        最低 4 byte (処理系依存)
long       4 byte
long long  8 byte

c / C の使い方は簡単です。

pack("cc", 65, 66) => "AB"
pack("c3", 65, 66, 67) => "ABC"
pack("c*", 65, 66, 67, 68) => "ABCD"

unpack("cc", "AB") => (65, 66)
unpack("c3", "ABC") => (65, 66, 67)
unpack("c*", "ABCD") => (65, 66, 67, 68)

数値 65 はアスキーコードの 'A' に該当します。そのほかの型指定文字を使う場合は、エンディアンに注意してください。大きな整数は 1 バイトでは表現できないので、数バイト使って表します。このときのバイトの並びをエンディアンといい、リトルエンディアンとビッグエンディアンの 2 種類があります。たとえば、1094861636 (0x41424344) を L で変換してみましょう。


0x41 はアスキーコードの 'A' に該当します。大きな位から順番に詰めていくのがビッグエンディアンで、小さな位から詰めていくのがリトルエンディアンです。これは使用している CPU によって異なり、インテル系の CPU はリトルエンディアンです。

pack("L", 0x41424344) => "DCBA"
unpack("L", "DCBA") => 1094861636 

エンディアンに左右されたくない場合は、n / N や v / V を使うといいでしょう。

n  ネットワークバイト順 (ビッグエンディアン) による short 値
N  ネットワークバイト順 (ビッグエンディアン) による long 値
v  VAXバイト順 (リトルエンディアン) による short 値
V  VAXバイト順 (リトルエンディアン) による long 値
pack("N", 0x41424344) => "ABCD"
unpack("N", "ABCD") => 1094861636 

pack("V", 0x41424344) => "DCBA"
unpack("V", "DCBA") => 1094861636 

整数値の変換では、0 と 1 だけからなる文字列を 2 進数とみなして数値に変換する b / B や、16 進数文字列を数値に変換する h / H もあります。

b  ビットストリング、下位ビットから上位ビットの順
B  ビットストリング、上位ビットから下位ビットの順
h  16進数文字列、下位ニブルが先
H  16進数文字列、上位ニブルが先

ニブルとは、16 進数 0xab の a や b、つまり 4 ビットデータのことを指します。これらの型指定文字で数字を指定する場合、取り込むビットやニブルの個数を表します。そして、与えられた文字列の先頭から 1 文字ずつ取り出して、次のように変換します。


pack("B*", "01000001") => "A"
pack("b*", "1000001") => "A"
unpack("B*", "A") => "01000001"
unpack("b*", "A") => "10000010"

pack("h*", "344556") => "CTe"
pack("H*", "344556") => "4EV"
unpack("h*", "CTe") => "344556"
unpack("H*", "4EV") => "344556"

実数については d と f があります。

d  倍精度浮動小数点数
f  単精度浮動小数点数
unpack("d", "62345678") => 6.82132005170133e-38
pack("d", 6.82132005170133e-38) => "62345678"

これらの形式は機種に依存するため、あるマシンで pack した浮動小数点数がほかのマシンでは正しく unpack できない可能性があります。

次は文字列の変換です。

a  アスキー文字列、ヌル文字を詰める
A  アスキー文字列、空白を詰める

a / A は末尾に必要なだけヌル文字か空白を追加して、指定した長さの文字列にして返します。a / A の後ろに数値を指定すると、引数の文字列から必要な個数だけ文字を取り出します。* が指定されると、引数の文字列をそのまま返します。

pack("A", "abcd")  => "a"
pack("A3", "abcd") => "abc"
pack("A8", "abcd") => "abcd    "
pack("A*", "abcd") => "abcd"

a / A を複数指定すると、引数 (リスト) の文字列に対して順番に型指定文字 (a / A) を適用します。型指定の間に空白を入れてもかまいません。それは無視されます。

pack("AAA", "abcd", "efg", "hi")      => "aeh"
pack("A2 A2 A2", "abcd", "efg", "hi") => "abefhi"
pack("A A* A4", "abcd", "efg", "hi")  => "aefghi  "

これは a / A だけの仕様で、他の型指定文字 (b/B, h/H を除く) の場合、指定した回数だけリストから要素を取り出して変換し、* は残りの要素をすべて変換することを意味します。ご注意くださいませ。

このほかにも次のような型指定文字があります。

p  文字列へのポインタ
P  構造体 (固定文字列) へのポインタ
u  uuencode した文字列
x  ヌルバイト
X  1 バイト後退する
@  絶対位置までヌルバイトを詰める

ポインタを扱う型指定文字は、UNIX のシステムコールを呼び出すために用意されているようですが、詳しい使い方はよくわかりません。ごめんなさい。もっとも、使う機会はほとんどないように思います。

●ファイルのダンプ

それでは簡単な例題として、ファイルを 16 進数でダンプするプログラムを作ってみましょう。出力するのは数値データのみで、文字データは出力しません。オプションによって、表示する数値データをバイト単位、2 バイト単位、4 バイト単位に変更します。ファイルはバイナリモードでアクセスし、16 byte ずつリードして unpack で分割します。オプションとテンプレート文字列は次のようになります。

c : バイト単位           => unpack( "C16", $buffer );
s : 2 バイト単位(short)  => unpack( "S8",  $buffer );
l : 4 バイト単位(long)   => unpack( "L4",  $buffer );

16 byte のデータを分割するのですから、c では 16 個、s は 8 個、l は 4 個のデータとなります。

データの読み込みは関数 read を使います。

read ファイルハンドル 変数 サイズ

ファイルハンドルからサイズバイトのデータを読み込み、指定した変数にセットします。read は実際に読み込んだデータ数を返し、ファイルの終わり (EOF) に達したならば 0 を返します。リードエラーが発生した場合は未定義値を返します。プログラムは次のようになります。

リスト : ファイルをダンプする

use strict;
use warnings;

my $type     = shift;
my $filename = shift;
my $address  = 0;
my $pat;

if (lc($type) eq "l") {
    $type = "L4";
    $pat  = "%08X ";
} elsif (lc($type) eq "s") {
    $type = "S8";
    $pat  = "%04X ";
} elsif (lc($type) eq "c") {
    $type = "C16";
    $pat  = "%02X ";
} else {
    die "Unknown option $type\n";
}

open my $in, "$filename" or die "Can't open $filename\n";
binmode $in;
while (read $in, my $buffer, 16) {
    printf "%08X  ", $address;
    foreach my $n (unpack $type, $buffer) {
        printf $pat, $n;
    }
    print "\n";
    $address += 16;
}
close $in;

最初にオプションとファイル名を変数にセットし、オプションのチェックを行います。関数 lc は文字列を小文字に変換します。変数 $type には unpack で指定するテンプレート文字列をセットし、$pat には printf で指定する書式文字列をセットします。%X は英大文字で 16 進数を表示します。$address はアドレスを表示するために使います。

次に、open でファイルをオープンして、binmode でバイナリモードに切り替えます。次の while ループで、read を呼び出してファイルからデータを読み込みます。読み込んだデータは $buffer にセットされます。次に、printf でアドレスを表示します。それから、unpack で整数値に分割し、foreach でデータをひとつずつ取り出して、printf で出力します。1 行分出力したら、改行コードを出力して $address を更新します。

このダンプを Windows で実行するとリトルエンディアンとして扱われます。エンディアンを指定できるように改造すると、データの解析に利用できるかもしれません。興味のある方はプログラムを改造してみてください。


Copyright (C) 2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Perl | NextPage ]