Perl 初心者やたまにしか Perl を使わない人向けのツボ

Perl 初心者やたまにしか Perl を使わない人向けのツボ

#はじめに
Perl はクセのある言語ですが、ちょっとしたツールなどを書くには便利な言語です。でも、たまにしかつかわなかったり、あまり慣れていないといろいろお約束やクセを忘れてしまい、短いプログラムを作るにもトラブってしまうことがあります。

ここでは、そういう自分のための情報をまとめてみました。


#コンソールアプリの基本
コンソールアプリの基本ですが、次のようになります。

```Perl
#!/usr/bin/env perl あるいは #!/usr/bin/perl
#上の先頭行は Windows ではなくてもよい。Linux の場合、ないとコマンドとして実行できない。行末は必ず LF のこと。
# おまじないだが、バグ対策になる2行。
use strict;
use warnings;

# コマンドライン引数の処理
if ($#ARGV < 0) {
  # コマンドライン引数がない場合
  print "エラーメッセージ\n";
  exit 1;
}

# コマンドライン引数(先頭)を表示する。
print $ARGV[0], "\n";

# なくてもよいが終了コードを明示するなら書いておく。
exit 0;
```


#先頭行 (インタプリタ指定行) の注意
先頭行はたいていの Linux ディストリビューションでは次のようになる。

```Perl
#!/usr/bin/env perl
あるいは #!/usr/bin/perl
```
ここで気を付けなくてはならないのが行末で、Windows のエディタで作成したソースを Linux で実行しようとするとエラーになることがある。これは行末が CR LF になっているためであり、インタプリタが CR までを含むとみなされてエラーになる。Windows で作成したソースは必ず行末を LF だけで保存するか、sed コマンドなどを利用して CR を削除しておく必要がある。

> bash: ./hello.pl: /usr/bin/perl^M: 誤ったインタプリタです: そのようなファイルやディレクトリはありません

行末を CRLF から LF にするコマンドの例
> sed -i "s/\r//" $1

なお、これは Perl 固有の問題ではなく、他の言語でも発生する。


#文字コードの注意
Windows で作成したソースを Linux で実行しようとすると文字化けが発生することがある。これは、Windows でソースをファイル保存するとき、Shift_JIS で保存されたためであることが多い。たいていの Linux ディストリビューションでは、デフォルトの文字コードが UTF-8 なので正しく表示できないためである。

iconv や nkf コマンドで文字コードを変換して文字化けがなくなるか確認する。

>iconv -f sjis -t utf8 \$1 あるいは nkf -w \$1

なお、これは Perl 固有の問題ではなく、他の言語でも発生する。


# 最初のおまじない
Perl のサンプルによく出てくる use strict だが、この行があるかないかで Perl コンパイラの挙動が異なる。ない場合は、古い Perl のバージョンとの互換モードになり、独特の挙動になる。

例えば、use strict がないと、my や our で変数を宣言する必要がない代わりに、すべての変数はグローバルになる。

```Perl
#!/usr/bin/perl

sub fun {
  $y = 100;  # サブルーチンの中なのにローカルにならない。(python などと動作が異なる)
}

fun();

print "$y\n";  # 100 と表示される。
```

上のプログラムで2行目に use strict; を挿入して実行すると、次のようなエラーメッセージが表示される。

> Global symbol "$y" requires explicit package name at ./test.pl line 5.

use warnings は警告を表示する指定で perl コマンドの -w オプションを指定するのと同じである。この指定をしておくと、エラーではないがエラー一歩手前の怪しいコードがあると警告を出してくれる。

よって、このおまじないの2行は黙って入れておいたほうが良い。


#$a, $b は常にグローバルなので注意
\$a, \$b はソートに使われる変数で常にグローバルであり注意が必要。下記の記事は \$a, \$b の使用例 (Qiita)

[Perl のグローバル変数 $a と $b の詳解](https://qiita.com/xtetsuji/items/33c77e1af4ad54bd8c38)

例えば、\$a はグローバルとして宣言済みなので、次のプログラムはエラーにならない。

```Perl
#!/usr/bin/perl
use strict;

$a = 1;
print "$a\n";
```


#print は何が表示されるかわからない ($_ に注意)
Python2 で print (Python3 では print()) を実行すると常に改行が出力される。ところが Perl では何が出力されるか プログラムにより異なる。

というのは、Perl では文や関数でパラメータを省略したときは、特別なグローバル変数 \$_ がパラメータとして 仮定されるという約束があるためである。

つまり print; は次の文と同じである。

print \$_;

\$_ が仮定されるのは print に限ったことでなく様々な場面で現れる。この約束を忘れると、 サンプルプログラムの意味を理解できなかったり、\$_ の値を誤って書き換えてしまい期待と違う結果を得たりしてしまう。


#BOOL 値の真は 1 だが偽は 0 以外にもある
BOOL 値を返す関数の値は真は 1 だが、偽は、'' や undef を返すときがある。例えば defined という関数は perldoc では

> 左辺値 EXPR が未定義値 undef 以外の値を持つか否かを示す、 ブール値を返します。

と、説明されているが、実際、次のようなコードを実行すると何も表示されない。この場合は defined のパラメータとして $_ が使用されるが、値を設定していないので「偽」が返される。

つまり、何も表示されないということは偽の値が 0 でなく '' か何かだということである。一方、print の前に "$_ = 123;" などと書いておくと 1 が表示される。つまり「真」は 1 ということになる。

```Perl
#!/usr/bin/env perl
use strict;
use warnings;

print defined, "\n";
それでは真は常に 1 なのかというと、そうでもなくて次のコードを実行すると 1 (最初のprint文) と「真」(2番目のprint文) が表示される。

#!/usr/bin/env perl
use strict;
use warnings;

$_ = 'A';
print defined, "\n";

if ($_) {
  print "真\n";
}
else {
  print "偽\n";
}
```

ここで言いたいことは、特定の１つの値 (例えば 0) を比較することは真偽判定として正しく動作しないということである。


#日本語を表示しようとすると警告が出る
Linux 環境でで文字コード設定が UTF-8 になっているのに、状況により日本語を表示しようとすると警告が出ることがある。

> Wide character in print at ....

その場合はプログラムの先頭の方に次の１行をおまじないとして入れると警告が消えることがある。これは標準出力へは常に UTF-8 でエンコードされていることを意味する。

> binmode(STDOUT, ":encoding(utf8)");

#配列の定義には () と [] がある
配列の初期値を設定するときは () に配列の要素を列挙するのが普通だが、() でなく [] を使っている例も見かけることがある。 () は配列そのものを意味するが、[] は配列の参照を意味する。例えば、Python では [] しかなく、そういう意識は不要なので Perl での混乱の元となる。

連想配列も () と {} で初期値を定義できるが、{} はやはり参照である。

次の例で、\$refa は参照なので @ でなく \$ が付いている。

```Perl
use strict;

my @arr = (1, 2, 3);
my $refa = [10, 20, 30];
my %hash = (A=>'a', B=>'b');
my $refh = {A=>'c', B=>'d'};

print $arr[1], "\n";   # 2
print $refa->[2], "\n";  # 30
print $hash{A}, "\n";  # a
print $refh->{A}, "\n"; # c
```

#比較を行う場合、演算子は２種類ある
Python などでは、変数の型に関係なく比較演算子は「等しい」は　== を使用する。 ところが、Perl では数値の「等しい」は == を使うが、文字列の「等しい」は eq を使う。


#関数(サブルーチン)には仮引数がない
他の言語で関数を定義するとき、関数名の後に「仮引数」のリストを書く。ところが、Perl にはそれがない。たまに他の言語のような書き方を見かけるとしても、それは「実験的」な実装で将来の保証がない。

Perl では関数(サブルーチン)の引数は、特殊な配列 @_ に入って渡される。関数(サブルーチン)の最初の方でよく見かけるコードは、@_ からパラメータを取り出しているものである。(下の例参照)

```Perl
sub isdigit {
  my $c = shift;  # パラメータ $c を特殊な配列 @_ から取り出している。
  return False if ($c eq '');
  my $a = ord($c);
  if (($a >= 0x30) && ($a <= 0x39)) {
    return 1;
  }
  else {
    return 0;
  }
}
```

このコードで shift は配列の要素を先頭からシフトして取り出す関数で、引数 @_ は省略されている。

@_ からのパラメータの受け取り方はこれだけではなくいろいろな方法がある。 しかし、関数の最初の方でやっている処理は、パラメータの取り出し処理のはずである。


#文字列リテラル "..." と '...' とは内容が異なる場合がある
Python では文字列リテラル "..." と '...' は同じである。しかし、Perl では "" はエスケープシーケンスを解釈するが、'' は書かれた内容そのものが文字列の内容となる。

例えば、\n が含まれていると "" では改行 (LF) とみなされるが、'' では単なる \ と n という文字とみなされる。


#文字列処理は正規表現処理が基本
Python などでは文字列処理関数がいろいろ用意されている。例えば、文字列の一部を置換するには、replace, translate, re.sub, re.subn などがあり、状況に応じて使い分けることができる。

しかし、Perl では replace 関数というのはなくて、正規表現を使いこなして行う。よって、文字列処理ではまず正規表現の活用を考える必要がある。

なお、Perl で文字列の置き換えは普通、正規表現の s 演算子を使用する。

```Perl
#!/usr/bin/perl
use strict;

my $str = "a;bc;def;ghi";

# セミコロンをコロンにする。
$str =~ s/;/:/g;
print "$str\n";
```

#変数にはスカラーと参照があるので意識して使わないとエラーになる
Perl では数値や文字列など普通の変数には \$ を付ける。そして、配列には @ を付け、連想配列には % を付ける。ところが参照は普通の変数と同じく \$ を付けるので、この変数は「参照」であると覚えておいて使わないと実行時にエラーになる。


#バージョンにより実験的に実装された機能がある(互換性が保証されない)
他の言語では、あるバージョンで実装された機能はだいたい後のバージョンにも引き継がれて、仮にその機能が削除されるとしても十分な周知期間が置かれて警告が表示されるなどする。

Perl のバージョンでは新機能と実験的機能があり、新機能は後のバージョンに引き継がれるが、実験的機能は引き継がれるとは限らず十分な周知期間なしでバッサリ削除されることがあるので注意が必要である。


#ファイルを行ごとに読み込むと改行が付いている
ファイルを行ごとに読み込むと行末には \n (Windows では\r\n) が付いてくる。多くの言語ではこの改行文字は自動的に取り除かれるが Perl では手動で chomp 関数を使って取り除く必要がある。

chomp の使用例

```Perl
my @arr = ();
open(FH, $filePath);
while () {
  chomp;  # $_ に行が入っている。
  push @arr, $_;
}
close(FH);
```

#１行のブロックでも中カッコを省略できない
C 言語系の言語だとブロック内の文が１行だけだと中カッコを省略できる。しかし、Perl は省略できないの注意が必要である。

C 言語だと中カッコを省略できるが・・・

```C
if (x > 0)
  printf("Yes.\n");
else
  printf("No.\n");
```

Perl だと中カッコを省略できない。

```Perl
if ($x > 0) {
  print "Yes.\n";
}
else {
  print "No.\n";
}
```
 

END