付録C：解答
---------------------
目次
■1.1：練習問題
■1.2：練習問題
■1.3：練習問題
■1.4：練習問題
■2.1：練習問題
■2.2：練習問題
■2.3：練習問題
■2.4：練習問題
■2.5：練習問題
■3.1：練習問題
■3.2：練習問題
■3.3：練習問題
■3.4：練習問題
■3.5：練習問題
■4.1：練習問題
■4.2：練習問題
■4.3：練習問題
■4.4：練習問題
■4.5：練習問題
■5.1：練習問題
■5.2：練習問題
■5.3：練習問題
■6.1：練習問題
■6.2：練習問題
■6.3：練習問題
■6.4：練習問題
■7.1：練習問題
■7.2：練習問題
■7.3：練習問題
■7.4：練習問題
■7.5：練習問題
■8.1：練習問題
■8.2：練習問題
■8.3：練習問題
■8.4：練習問題
■8.5：練習問題
■9.1：練習問題
■10.1：練習問題
■10.2：練習問題
■10.3：練習問題
■11.1：練習問題
■11.2：練習問題
■11.3：練習問題
■11.4：練習問題
---------------------

■1.1：練習問題
1.	画面に-0.123と表示するスクリプトを直接コマンドラインに記述して実行しなさい。また、同じ動作をするスクリプトファイルを作成して実行しなさい。
【答】
コマンドライン:
perl -e 'print -0.123;'

スクリプト:
print -0.123;

2.	次のスクリプトファイルは正しく動作しますか。

print 1
23;

【答】
エラーになります。123は1つのオプション値と見なされますが、オプションの値の途中でスペースを入れてはいけません。

3.	次のスクリプトファイルは正しく動作しますか。

print 123,45,
678;

【答】
正しく動作します。「,」は複数のオプションを区切るために使われる記号ですので、123、45、678という3つの値を指定したものと見なされます。「,」の前後にはスペースを入れることができます。

■1.2：練習問題
1.	変数$nameにあなたの名前を入れ、それを表示した後で改行するスクリプトを作りなさい。
【答】
$name = "Yamada Taro";
print "$name\n";

2.	次のスクリプトを実行するとどのような結果が得られますか。

$hex = "\x2a";
$abc = "ABC";
$quote = '\n';
print "$hex$quote$abc\n";
【答】
*\nABCと表示されます。$quoteの値'\n'は単一引用符で囲まれているため、そのままの文字として扱われます。この値はそのあとに二重引用符で囲まれても変わることはありません。

3.	次のスクリプトを「Hello, Tarokun.」と表示されるように修正しなさい。

$hello = "Hello";
$name = "Taro";
print "$hello,$namekun\n";

【答】
次のように修正します。

$hello = "Hello";
$name = "Taro";
print "$hello,${name}kun\n";

「,」は変数名には使われない文字なので、$helloを{ }で囲む必要はありません。

4.	次のスクリプトをqqを使って書き換えなさい。

print "二重引用符 \"\" は \\\"\\\" で示します。\"こんにちは\"\n";

【答】
次のようにします。

print qq/二重引用符 "" は \\"\\" で示します。"こんにちは"\n/;

■1.3：練習問題
1.	今まで紹介したスクリプトにコメントを付けなさい。
【答】
（省略）

2.	次のスクリプトを実行するとどのような結果になりますか。

print 12,
# 34,
56, "\n";

【答】
正しく動作し、1256と表示されます。

■1.4：練習問題
1.	ユーザーに姓と名を別々に入力させた後、「こんにちは、ユーザー名さん。」と挨拶を返すスクリプトを作りなさい。ただし、姓と名の間にはスペースを入れないこととします。
【答】	# 姓と名を入力させて、挨拶を返すスクリプト
#  hello2.pl

# 姓を入力
print "あなたの姓は何ですか: ";
# ユーザーの入力を待つ
$surname = <STDIN>;
# 改行文字を除く
chomp($surname);

# 名を入力
print "あなたの名は何ですか: ";
# ユーザーの入力を待つ
$firstname = <STDIN>;
# 改行文字を除く
chomp($firstname);

# 挨拶を表示する
print "こんにちは、$surname$firstname さん。\n";

2.	「りんご」を英語で何と呼ぶか（正解はapple）という質問をして、ユーザーの入力が正解かどうかによって異なるメッセージを表示するスクリプトを作りなさい。
【答】	# りんごの英語名のクイズスクリプト
#  quiz2.pl

print "「りんご」は英語で何と呼びますか: ";
# ユーザーの入力を待つ
$answer = <STDIN>;
# 改行文字を除く
chomp($answer);

# 処理を分岐
if ($answer eq "apple") {
  # eqは文字列が等しいことを意味する
  # "apple"に等しいとき
  print "あたりです!\n";
} else {
  # "apple"に等しくなかったとき
  print "ざんねん、はずれです。\n";
}

■2.1：練習問題
1.	次のスクリプトを実行するとどのような結果が表示されますか。

# 文字列を含んだ数値
#  mix_number.pl

print (("20a1" + 5 . 1), "\n");

【答】
251
まず文字列「20a1」は数値20と認識されます。それに5を加算して25となった後、「.」演算子で1が文字列として連結されています。

2.	*という文字を効率良く100個表示するスクリプトを作りなさい。
【答】
# *を100文字表示
# asterisk.pl

print "*" x 100, "\n";

3.	次の式の計算結果を表示するスクリプトを作りなさい。

 1
───
 2     ×  10×2
───    ────
3 + 1     −2−10

【答】
# 式の実行
#  equation.pl

print ((1 / 2 / (3 + 1) * 10 * 2 / (-2 - 10)), "\n");

4.	次のスクリプトを実行するとどのような結果が表示されますか。

# 優先度
#  minus.pl

$number = -2 ** 4;
print "-2 ** 4 = $number\n";

【答】
-2 ** 4 = -16
累乗は単項マイナスよりも優先されます。このため、-2の4乗ではなく2の4乗を行った後、値に対して単項マイナスが実行されます。

■2.2：練習問題
1.	次のスクリプトの加算、減算をインクリメント演算子やデクリメント演算子で置き換えなさい。

# インクリメント演算子を使用していないスクリプト
#  replace_original.pl

$number1 = 0;
$number2 = 0;

$number1 = $number1 + 1;
$number2 = $number1;

# 表示
print "インクリメント:\t数値1: $number1 数値2: $number2\n";

$number2 = $number1;
$number1 -= 1;

# 表示
print "デクリメント:\t数値1: $number1 数値2: $number2\n";

【答】
# インクリメント/デクリメント演算子で置き換えたスクリプト
#  replace_increment.pl

$number1 = 0;
$number2 = 0;

$number2 = ++$number1;

# 表示
print "インクリメント:\t数値1: $number1 数値2: $number2\n";

$number2 = $number1--;

# 表示
print "デクリメント:\t数値1: $number1 数値2: $number2\n";

2.	次のスクリプトを実行すると、どのような結果が表示されますか。

# インクリメント演算子のテスト
#  increment.pl

$number1 = 1;
$number2 = $number1++;
$number3 = ++$number1;

print "N1:$number1, N2:$number2, N3:$number3\n";

【答】
N1:3, N2:1, N3:3
$number1は、2行目で値を$number2に代入したあとでインクリメントされ2になります。3行目では、$number1がインクリメントされて3になったあと、$number3に値が代入されます。

■2.3：練習問題
1.	2と10という2つの値に対して、<演算子を実行したときとlt演算子を実行したときの違いを調べるスクリプトを作りなさい。
【答】
# <演算子とlt演算子の違い
#  smaller_and_lt.pl

# <演算子
print "<演算子による評価\n";
if (2 < 10) {
  print "2より10が大きい\n";
} else {
  print "10より2が大きい\n";
}

# lt演算子
print "lt演算子による評価\n";
if (2 lt 10) {
  print "2より10が大きい\n";
} else {
  print "10より2が大きい\n";
}

このスクリプトの実行結果は次のとおりです。

<演算子による評価
2より10が大きい
lt演算子による評価
10より2が大きい

<演算子は数値の大小、lt演算子は文字コードの大小をもとに評価を行います。lt演算子による評価が「10より2が大きい」となるのは、最初のマッチングで「2」と「1」が比較され、2の方が文字コードが大きい、と判断されるためです。

2.	入力された文字列に「鈴木」という単語が入っていないことを確認するスクリプトを作りなさい。
【答】
# 否定パターンマッチ
#  antipattern.pl

print "あなたの名前は何ですか: ";
$string = <STDIN>;
chomp($string);

# 文字列に「鈴木」が含まれていないことを調べる
if ($string !~ /鈴木/) {
  print "こんにちは。\n";
} else {
  print "鈴木さんはアクセスできません。\n";
}

■2.4：練習問題
1.	2つの値を入力すると、その論理和を表示するスクリプトを、代入演算子を使って作りなさい。
【答】	# 2値の論理和
#  logical_or.pl

print "1つ目の数値を入力してください: ";
$number1 = <STDIN>;
chomp($number1);
print "2つ目の数値を入力してください: ";
$number2 = <STDIN>;
chomp($number2);

# 文字列を数値にする
$number1 += 0;
$number2 += 0;

# 論理和を代入
$number1 |= $number2;

print "論理和: $number1\n"

2.	ユーザー名とパスワードの入力を要求し、ユーザー名がSECRETUSER、パスワードがSECRETと合致すれば「認証しました」、どちらかが合致しなかったら「拒否しました」と表示するスクリプトを作りなさい。
【答】
# ユーザー名とパスワードの認証
#  login.pl

print "ユーザー名を入力してください: ";
$username = <STDIN>;
chomp($username);
print "パスワードを入力してください: ";
$password = <STDIN>;
chomp($password);

# 判定
if ($username eq "SECRETUSER" && $password eq "SECRET") {
  print "認証しました。\n";
} else {
  print "拒否しました。\n";
}

3.	2つの値の入力を受け取り、それらの値が共に5よりも大きく、同時に1つ目の値が2つ目の値より大きければ「真です。」、そうでなければ「偽です。」と表示するスクリプトを作りなさい。
【答】
# 短絡論理演算子で2値を3つの論理で比較する
#  multiple_shortcut.pl

print "1つ目の数値を入力してください: ";
$number1 = <STDIN>;
chomp($number1);
print "2つ目の数値を入力してください: ";
$number2 = <STDIN>;
chomp($number2);

# 比較する
if ($number1 > 5 && $number2 > 5 && $number1 > $number2) {
  print "真です。\n";
} else {
  print "偽です。\n";
}

■2.5：練習問題
1.	ヒア演算子を使うと、「'」や「"」を表示するのにエスケープシーケンスを使う必要がないことを確かめなさい。
【答】
# 'や"をヒア演算子で使う
#  here5.pl

print <<EOT
  単一引用符 '
  二重引用符 "
EOT
  ;

このスクリプトの実行結果は次のようになります（エスケープシーケンスを指定しなくても、正しく表示されることが確認できます）。

  単一引用符 '
  二重引用符 "

2.	ヒア演算子で文字列を受け取り、それを5回繰り返して表示するスクリプトを作りなさい。
【答】
# ヒア演算子の文字列を繰り返す
#  here6.pl

$name = "鈴木さん";

print <<"EOT"
こんにちは、$name。
こんばんは、$name。
EOT
x 5;

■3.1：練習問題
1.	2つの値と記号（+、-、*、/）を入力すると、それらの値を指定した記号で計算するスクリプトを作りなさい。
【答】
# 計算機スクリプト
#  calc.pl

print "値1: ";
$value1 = <STDIN>;
print "値2: ";
$value2 = <STDIN>;
print "計算内容(+,-,*,/): ";
$calc = <STDIN>;
chomp($value1, $value2, $calc);

if ($calc eq "+") {
  # 加算
  $return = $value1 + $value2;
} elsif ($calc eq "-") {
  # 減算
  $return = $value1 - $value2;
} elsif ($calc eq "*") {
  # 乗算
  $return = $value1 * $value2;
} elsif ($calc eq "/") {
  # 除算
  unless ($value2 == 0) {
    $return = $value1 / $value2;
  } else {
    # 0 による除算
    $return = "0 で除算しようとしました。";
  }
} else {
  # どれでもなかったとき
  $return = "無効な計算内容です。";
}

# 結果表示
print "計算結果: $return\n";

2.	次のパスワード認証スクリプトを、unlessステートメントで書き換えなさい。

# パスワード認証スクリプト
#  password.pl

print "パスワードを入力: ";
$password = <STDIN>;
chomp($password);

if ($password eq "SECRET") {
  print "認証に成功しました。\n";
} else {
  print "認証に失敗しました。\n";
}

【答】
# パスワード認証スクリプト
#  password_unless.pl

print "パスワードを入力: ";
$password = <STDIN>;
chomp($password);

unless ($password ne "SECRET") {
  print "認証に成功しました。\n";
} else {
  print "認証に失敗しました。\n";
}

3.	次のスクリプトを実行すると、どのような結果が表示されますか。

# unlessの動作
#  unless.pl

$number1 = 2;
$number2 = 1;
unless ($number1 == 2 || $number2 != 1) {
  print "OR\n";
}
unless ($number1 == 2 && $number2 != 1) {
  print "AND\n";
} 

【答】
AND
	if文に比べてややこしく見えますが、unlessは式の結果が偽のときに実行されるということさえ覚えておけば理解できるはずです。

■3.2：練習問題
1.	次のスクリプトを実行して2を入力すると、どのような結果が表示されますか。

# 倍々計算
#  double.pl

print "数値を入力してください: ";
$number = <STDIN>;
chomp($number);

until ($number > 16) {
  print "数値: $number\n";
  $number += $number;
}

【答】
数値: 2
数値: 4
数値: 8
数値: 16	

2.	「SECRET」と入力されるまで、「パスワードを入力してください:」と繰り返し表示して、ユーザーに入力を要求するスクリプトを作成しなさい。
【答】
# パスワードの問い合わせ
#  secret.pl

do {
  print "パスワードを入力してください: ";
  $password = <STDIN>;
  chomp($password);
} until ($password eq "SECRET");

	do-whileを使って記述することもできます。

3.	do-untilステートメントを使って、指定した10進数を2進数に変換するスクリプトを作りなさい。2進数とは、0と1だけで表した数値のことです。たとえば2進数の「10」は10進数の「2」を意味し、2進数の「1011」は10進数の「11」を意味します。

ヒント 数値を2で割ったときの余りの値を格納してから、数値を2で割ったときの商を再びテストします。これを商が1より小さくなるまで繰り返します。

【答】
# 10進数2進数変換
#  bit.pl

print "10進数の数値を指定してください: ";
$number = <STDIN>;
chomp($number);

$bin = '';

do {
  # 2で割った余りを入れる
  $bin = ($number % 2) . $bin;
  # $numberを2で割る
  $number /= 2;
  # $numberが1より小さくなるまで繰り返す
} until ($number < 1);

print "2進数: $bin\n";

■3.3：練習問題
1.	次のスクリプトをforステートメントを使って書き換えなさい。

# デクリメント
#  decrement_until.pl

print "数値を入力してください: ";
$number = <STDIN>;
chomp($number);

until ($number < 0) {
  print "数値: $number\n";
  $number--;
}

【答】
# デクリメント
#  decrement_for.pl

print "数値を入力してください: ";
$number = <STDIN>;
chomp($number);

for ($i = $number; $i >= 0; $i--) {
  print "数値: $number\n";
  $number--;
}

2.	指定した数値までの数をカウントするスクリプトを作りなさい。ただし、4と13だけは表示しないようにしなさい。
【答】
# 指定した数まで表示、4と13は飛ばす
#  for_skip4_13.pl

print "数値を入力してください: ";
$max = <STDIN>;
chomp($max);

for ($number = 1; $number <= $max; $number++) {
  if ($number == 4 || $number == 13) {
    # $numberが4および13のときには1つインクリメント
    $number++;
  }
  print "数値: $number\n";
}

3.	1から数値を2倍ずつしていき、64になるまで表示するスクリプトを作りなさい。
【答】
# 1から64まで倍数を表示
#  for_double.pl

for ($number = 1; $number <= 64; $number *= 2) {
  print "数値: $number\n";
}

■3.4：練習問題
1.	指定した数値までの数をカウントするスクリプトを作りなさい。ただし、nextステートメントを使って、4と13だけは表示しないようにしなさい。
【答】
# 指定した数まで表示、nextを使って4と13は飛ばす
#  next_skip4_13.pl

print "数値を入力してください: ";
$max = <STDIN>;
chomp($max);

for ($number = 1; $number <= $max; $number++) {
  if ($number == 4 || $number == 13) {
    # $numberが4および13のときにはnextを実行
    next;
  }
  print "数値: $number\n";
}

2.	次のスクリプトを実行すると、どのような結果が表示されますか。

# ラベルを使ったループ
#  label2.pl
LP1: for ($i=0; $i<=5; $i++) {
  LP2: for ($j = 0; $j <= $i; $j++) {
    if ($i == 2 && $j == 2) {
      print " [IF] ";
      $i += 2;
      redo LP1;
    }
     print "$j-";
  }
  print "\n";
}

【答】
0-
0-1-
0-1- [IF] 0-1-2-3-4-
0-1-2-3-4-5-

■3.5：練習問題
1.	入力された文字列がSECRETと等しければ「認証に成功しました。」、それ以外であれば「認証に失敗しました。」と表示するパスワード認証スクリプトを、?演算子を使って作りなさい。
【答】
# ?演算子によるパスワード認証スクリプト
#  password.pl

print "パスワードを入力: ";
$password = <STDIN>;
chomp($password);

print (($password eq  "SECRET") ?
  "認証に成功しました。\n" : "認証に失敗しました。\n"); 

2.	2つの値を入力すると、1つ目の値を2つ目の値で割った結果を表示するスクリプトを作りなさい。ただし、ステートメント修飾子を使って0 による除算を回避しなさい。
【答】
# 2値の除算、0除算はステートメント修飾子で回避する
#  divide0.pl

print "1つ目の数値を入力してください: ";
$number1 = <STDIN>;
chomp($number1);
print "2つ目の数値を入力してください: ";
$number2 = <STDIN>;
chomp($number2);

# unlessで除外する
print ($number1 / $number2) unless ($number2 == 0);
print "\n";

3.	次のスクリプトを、ステートメント演算子を使って短く書き直しなさい。

# 長いif文
#  long_if.pl

$number1 = 1;
$number2 = 0;
if ($number1 | $number2 == 1) {
  print "OR\n";
}
if ($number1 & $number2 == 1) {
  print "AND\n";
}
if ($number1 ^ $number2 == 1) {
  print "XOR\n";
}

【答】
# ステートメント演算子を使ったif文
#  statement_if.pl

$number1 = 1;
$number2 = 0;
print "OR\n" if ($number1 | $number2 == 1);
print "AND\n" if ($number1 & $number2 == 1);
print "XOR\n" if ($number1 ^ $number2 == 1);

■4.1：練習問題
1.	曜日として数値1〜7を入れると、それに対応して「Sun」〜「Sat」を表示するスクリプトを作りなさい。
【答】
# 入力した数に相当する曜日の表示
#  week.pl

# 週の名前の配列を設定
@week = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');

print "曜日を指定してください(1〜7): ";
$number = <STDIN>;
chomp($number);

# 相当する曜日の名前を表示
print ($week[$number - 1], "\n");

また、次のようなスクリプトも可能です。

# 入力した数に相当する曜日の表示
#  week2.pl

print "曜日を指定してください(1〜7): ";
$week = <STDIN>;
chomp($week);

# 相当する曜日の名前を表示
print (('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat')
  [$week - 1], "\n");

2.	数値−5から3、文字AからZまでを配列に格納して、それを表示するスクリプトを作りなさい。
【答】
# 範囲演算子を使ったリストの指定
#  region2.pl

# 数値と英字の範囲
@numbers_and_alphabets = (-5 .. 3, "A" .. "Z");
print @numbers_and_alphabets, "\n";

■4.2：練習問題
1.	push関数とpop関数を使って、配列の要素の順序を逆にして別の配列にコピーするスクリプトを作りなさい。
【答】
# pushとpopで配列を逆順にコピー
#  array_reverse.pl

# 元の配列
@original_array = (1 .. 9);
print "元の配列: ", @original_array, "\n";

# 取り出すものがなくなるまでwhileステートメントを繰り返す
while (@original_array) {
  # 1つずつ取り出し、コピー
  push(@reverse_array, pop(@original_array));
}

# 逆の配列を表示
print "逆順配列: ", @reverse_array, "\n";

	whileステートメントの条件式に配列@original_arrayを指定して、配列の要素がなくなるまでループを繰り返しています。whileループ内のpush関数では、第1引数に、要素を逆順に入れていくための新たな配列@reverse_arrayを指定しています。pushの第2引数は配列に追加する要素の指定ですが、ここではpop関数を指定しているため、この関数で元の配列@original_arrayの末尾から取り出した要素が新たな配列の末尾に順に追加されていき、結果的に逆順の配列が作られます。

2.	unshift関数とshift関数を使って、1.のスクリプトを書き換えなさい。
【答】
# shiftとunshiftで配列を逆順にコピー
#  array_reverse.pl

# 元の配列
@original_array = (1 .. 9);
print "元の配列: ", @original_array, "\n";

# 取り出すものがなくなるまでwhileステートメントを繰り返す
while (@original_array) {
  # 1つずつ取り出し、コピー
  unshift(@reverse_array, shift(@original_array));
}

# 逆の配列を表示
print "逆順配列: ", @reverse_array, "\n";

■4.3：練習問題
1.	ある数値配列の中を調べて、5より大きい値があればそれを表示するスクリプトを作りなさい。
【答】
# 配列の中に5より大きい数があれば表示する
#  larger.pl

@array = (1 .. 10);

foreach $number (@array) {
  if ($number > 5) {
    print "5より大きい数です: $number\n";
  }
}

2.	次のスクリプトを実行すると、どのような結果が表示されますか。

# foreachステートメントと配列の扱いのテスト
#  foreach3.pl

@numbers = (0 .. 3);
foreach $value (@numbers) {
    $value *= 2;
}
print @numbers, "\n";
print @numbers * 2, "\n";

【答】
0246
8
	1行目で@numbersには、0、1、2、3という4つの要素が格納されます。次のforeachでは、これらの要素が1つずつ順番に$valueに格納されるため、$value *= 2によってそれぞれの要素の値が2倍になります。したがって、foreachステートメントが終了したとき、@numbersには要素0、2、4、6が格納されています。
	1つ目のprintでは@numbersがリストとして扱われるため、各要素の値をつなげた文字列が表示されます。2つ目のprintでは@numbersがスカラー変数として扱われるため、要素の数4を2倍した8が表示されます。

■4.4：練習問題
1.	1〜100までの数値の配列を読み、2、3、5で割り切れる要素の数をそれぞれ調べて表示するスクリプトを、ハッシュを使って作りなさい。
【答】
# 配列のうち、2、3、5で割り切れる要素の数をそれぞれ調べる
#  divide.pl

@array = ( 1 .. 100 );

# 配列を処理
foreach $number (@array) {
  if ($number % 2 == 0) {
    # 2で割り切れる
    $total{'2'}++;
  }
  if ($number % 3 == 0) {
    # 3で割り切れる
    $total{'3'}++;
  }
  if ($number % 5 == 0) {
    # 5で割り切れる
    $total{'5'}++;
  }
}

# 結果を表示
print "2で割り切れる要素数: ", $total{'2'}, "\n";
print "3で割り切れる要素数: ", $total{'3'}, "\n";
print "5で割り切れる要素数: ", $total{'5'}, "\n";

■4.5：練習問題
1.	リスト("a","i","u","e","o")の各要素をキー、リスト("あ","い","う","え","お")の各要素を値とするハッシュを作り、キーのリスト、値のリスト、ペアの一覧をそれぞれ表示しなさい。
【答】
# keys、values、eachのテスト
#  keys_values_each2.pl

# a、i、u、e、oのリスト
@array1 = ("a", "i", "u", "e", "o");

# あ、い、う、え、おのリスト
@array2 = ("あ", "い", "う", "え", "お");

for ($i = 0; $i < @array1; $i++) {
  # ハッシュ%array3に格納
  $array3{$array1[$i]} = $array2[$i];
}

# キーリスト表示
print "キー:\n\t";
foreach $key (keys(%array3)) {
  print "$key ";
}

# 値リスト表示
print "\n値:\n\t";
foreach $value (values(%array3)) {
  print "$value ";
}

# ペア表示
print "\nペア:\n\t";
print "$key -> $value " while (($key, $value) = 
  each(%array3));
print "\n";

■5.1：練習問題
1.	入力された文字列から文字iおよびIを抜き出して、その数を表示するスクリプトを作りなさい。
【答】
# 文字「i」の数を表示
#  count.pl

print "文字列を入力してください: ";
$string = <STDIN>;
chomp($string);

# カウント(大文字小文字を区別しない)
$count++ while ($string =~ /i/gi);

# 表示
print "総個数: $count\n";

	iのパターンマッチにiオプションを指定しているので、大文字小文字の区別が無視されて、iとIの両方にマッチします。また、whileとgオプションを併用することで、繰り返し検索を行っています。

■5.2：練習問題
1.	入力された英文を単語ごとに区切り、「単語: 区切られた単語」のように表示するスクリプトを作りなさい。
【答】
# 文字列から単語を抽出
#  split.pl

print "文字列を入力してください: ";
$string = <STDIN>;
chomp($string);

# gオプションとwhileステートメントを使って単語を抜き出す
print "単語:$&\n" while ($string =~ /\w+/g);

2.	次のスクリプトを実行すると、どのような結果が表示されるか答えなさい。

# かっこによるマッチの取得
#  parent.pl

if ("abcdef" =~ /.(((.).)(.).)./) {
  # マッチした値の表示
  print "1=$1 2=$2 3=$3 4=$4\n";
}

【答】
1=bcde 2=bc 3=b 4=d

3.	次のリストが与えられたとき、dではじまりonで終わる単語のみを表示するスクリプトを作りなさい。

@array = ("nation",
          "solution",
          "distribution",
          "edition",
          "disk",
          "direct connection",
          "destruction",
          "dimension",
          "director",
          "donkey"
         );

【答】
# dではじまりonで終わる単語を表示する
#  foreach_d_on.pl

@array = ("nation",
          "solution",
          "distribution",
          "edition",
          "disk",
          "direct connection",
          "destruction",
          "dimension",
          "director",
          "donkey"
         );

foreach $value (@array) {
  print "$value, " if ($value =~ /^d\w*on$/);
}
print "\n";

4.	次のようなスクリプトがあります。「# ここに挿入」の箇所に、下記のケース1を挿入した場合とケース2を挿入した場合では、実行結果にどのような違いが生じますか。

# 最長一致と最短一致
#  longest_shortest.pl

$html = <<EOT;
<HTML>
  <HEAD>
    <TITLE>My Home Page</TITLE>
  </HEAD>
  <BODY>
    <H1>This is my home page</H1>
  </BODY>
</HTML>
EOT

# ここに挿入

ケース1
while ($html =~ /<.+>/g){
  print "$&\n";
}

ケース2
while ($html =~ /<.+?>/g) {
  print "$&\n";
}

【答】
ケース1の正規表現が、<で始まり、任意の文字列をはさんで>で終わる最長の文字列にマッチするのに対して、ケース2の正規表現は、<で始まり、任意の文字列をはさんで>で終わる最短の文字列にマッチします。したがって、ケース1、ケース2の実行結果は次のようになります。

ケース1
<HTML>
<HEAD>
<TITLE>My Home Page</TITLE>
</HEAD>
<BODY>
<H1>This is my home page</H1>
</BODY>
</HTML>
ケース2
<HTML>
<HEAD>
<TITLE>
</TITLE>
</HEAD>
<BODY>
<H1>
</H1>
</BODY>
</HTML>

■5.3：練習問題
1.	スペース文字で区切られた2つの入力文字列の順番を入れ替えるスクリプトを作りなさい。
【答】
# スペース文字で区切った2値を入れ替える
#  swap.pl

print "2つの単語をスペースで区切って入力してください: ";
$string = <STDIN>;
chomp($string);

# 入れ替え
$string =~ s/(\w+)\s+(\w+)/$2 $1/;

# 表示
print "$string\n";

2.	スペース文字で区切られた2つの数値の商を求めるスクリプトを、s演算子を使って作りなさい。

ヒント 置換の式の中で/を使うときには、\文字を付けてエスケープシーケンスにする必要があります。

【答】
# スペース文字で区切った2値の商を表示
#  divide.pl

print "2つの数値をスペースで区切って入力してください: ";
$string = <STDIN>;
chomp($string);

# eオプションを使って直接演算する
$string =~ s/(\-?\d+\.?\d*)\s+(\-?\d+\.?\d*)/$1 \/ $2/e;

# 表示
print "$string\n";

■6.1：練習問題
1.	10 進数の数値を入力すると4 桁の16 進数に変換して表示するスクリプトを、printf関数を使って作りなさい。ただし、表示する16進数が4桁に満たないときは、足りない部分に0を埋めるものとします。
【答】
# 入力された10進数の値を16進数にする
#  hex.pl

print "10進数の数値を入力してください: ";
$number = <STDIN>;
chomp($number);

# 16進数で表示
printf("16進数: %04x\n", $number);

2.	入力された文字列を1文字ずつに分割し、各要素間に「.」を入れて表示するスクリプトを、split関数とjoin関数を使って作りなさい。

ヒント split関数の正規表現を空にすると、文字列を1文字ずつ区切ることができます。

【答】
# 入力文字列を分割し、間に.を入れて表示
#  period.pl

print "文字列を入力してください: ";
$string = <STDIN>;
chomp($string);

# splitで1文字ずつに分割し、すぐにjoinで連結する
print join(".", split(//, $string)), "\n";

3.	文字列を逆さまにして表示するスクリプトをchop関数を使って作りなさい。たとえばPERLという文字列を指定すると、LREPを表示するようにします。

ヒント whileステートメント修飾子またはuntilステートメント修飾子を併用するとよいでしょう。

【答】
# 文字列を逆順にする
#  reverse.pl

print "文字列を入力してください: ";
$string = <STDIN>;
chomp($string);

# $stringが空になるまで末尾を取り出し、表示
print chop($string) while ($string);
print "\n";

4.	文字列要素を持つリストを定義して、各要素の文字数の合計を表示するスクリプトを作りなさい。
【答】
# リストの各要素の長さの合計を計算
#  list_length.pl

@array = ('Apple', 'Orange', 'Pine', 'Banana');

# 配列を処理
foreach $value (@array) {
  # 合計値に加算
  $sum += length($value);
}
print "合計:$sum\n";

■6.2：練習問題
1.	アークサインは、atan2(θ, sqrt(1-θ * θ))で求められます。ラジアン値を入力するとアークサイン値を表示するスクリプトを作りなさい。
【答】
# アークサインを求める
#  arcsin.pl

print "0〜1の範囲でラジアン値を入力: ";
$radian = <STDIN>;
chomp($radian);

# アークサインを計算
print ("アークサイン: ", 
  (atan2($radian, sqrt(1 - $radian * $radian))), "\n");

2.	サイコロの代わりになるスクリプトを、int関数とrand関数を使って作りなさい。

ヒント 6未満の乱数を作り、1を足します。
【答】
# サイコロ
#  dice.pl

# randとintで目を作る
print "ダイスロール: ", int(rand(6) + 1), "\n";

3.	logexの値をloge10で割ると、log10xを求めることができます。指定した数値のlog10を求めるスクリプトを作りなさい。
【答】
# log10を求める
#  log.pl

print "数値を入力してください: ";
$number = <STDIN>;
chomp($number);

# log10を計算して表示
print "log10: ", (log($number) / log(10)), "\n";

■6.3：練習問題
1.	現在から24時間前の時間情報を調べるスクリプトを作りなさい。ただし、うるう秒は考えないものとします。
【答】
# 24時間前の時間情報を取得し、表示
#  yesterday.pl

# 地方時で取得(1日分に相当する秒数をtimeから引く)
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday,
  $isdst) = localtime(time - (24 * 60 * 60));

# 整形して表示
printf("24時間前の地方時間:\t%s/%s/%s %s %02d:%02d:%02d\n",
  $year + 1900, ++$mon, $mday,
  ("日", "月", "火", "水", "木", "金", "土")[$wday],
    $hour, $min, $sec);

2.	生年月日を入力し、現在の月日と同じであれば年齢と祝福のメッセージを表示するスクリプトを作りなさい。
【答】
# 誕生日からの経過日数を導出する
#  birthday.pl

use Time::Local;

$tvalue = time;

# 誕生日年月日を取得
print "誕生日年月日を入力 (YYYY/MM/DD): ";
$string = <STDIN>;
chomp($string);
($byear, $bmonth, $bday) = split(/\//, $string);

# 現在の時刻を取得
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime( );

if (($bmonth - 1) == $mon && $bday == $mday) {
  print $year + 1900 - $byear,  "歳のお誕生日おめでとうございます。\n";
}

■6.4：練習問題
1.	値を入力すると、その値の平方根を表示するスクリプトを作成しなさい。ただし、負の値の平方根を求めようとするとエラーになるので、eval関数を使ってエラーを捕捉しなさい。
【答】
# エラー捕捉付きの平方根
#  eval_sqrt.pl

print "数値を入力してください: ";
$number = <STDIN>;
chomp($number);

# evalでsqrtを実行
eval("print sqrt($number), \"\\n\"");
print "エラーが発生しました。: $@" if ($@);

2.	reverse関数は使わず、sort関数とインラインサブルーチンのみを使って入力した数値のリストを大きい順にソートするスクリプトを作りなさい。

ヒント 逆転の発想です。

【答】
# スペース文字で区切った数値リストを逆順に整列する
#  sort_reverse2.pl

print "要素をスペース文字で区切った数値のリストを入力してください: ";
$numbers = <STDIN>;
chomp($numbers);

# ソートして表示
print join(", ", sort { $b <=> $a } (split(/\s+/, 
  $numbers))), "\n";

3.	0から255までの10進数の数値を入力すると、その数値を2進数に変換して表示するスクリプトを、pack関数を使って作りなさい。

ヒント pack関数を使って数値を一度パックしてから、unpack関数を使って適切なフォーマットで数値を取り出すと、2進数、10進数の変換などを行うことができます。

【答】
# 10進数から2進数への変換
#  bitcalc.pl

print "0から255までの数値を入力してください: ";
$num = <STDIN>;
chomp($num);

$packed = pack("I", $num);
$bin = unpack("B8", $packed);
print "2進数: $bin\n";

4.	クイズを出して、5秒後に答えを表示するスクリプトを作りなさい。
【答】
# 5秒後に答えを表示するクイズ
#  quiz_sleep.pl

$question = "世界で一番大きな湖は?\n";
$answer = "答え: カスピ海\n";

print $question;
        sleep(5);
print $answer;


■7.1：練習問題
1.	インチ数を入力するとcm単位に換算して返すサブルーチンを作成しなさい。

ヒント 1インチは2.54cmです。

【答】
# インチをcmに換算して表示する
#  inch.pl

print "変換するインチ数を入力してください: ";
$inch = <STDIN>;
chomp($inch);

print "$inch インチ = ", &inch_cm($inch), " cm\n";

# インチからcmに変換するサブルーチン
sub inch_cm {
  my($inch) = @_;
  $inch * 2.54;
}

2.	数値を入力するとその数値の階乗を返すサブルーチンを、再帰呼び出しを使って作成しなさい。階乗とは、1×2×3 ... ×Nの値のことです。たとえば4の階乗は1×2×3×4＝24になります。
【答】
# 階乗を求める
#  factorial.pl

print "階乗数を入力してください: ";
$number = <STDIN>;
chomp($number);

# 呼び出しと表示
print "$number! = ", &factorial($number), "\n";

# サブルーチン本体
sub factorial {
  my($number) = @_;

  if ($number <= 1) {
    # 1以下ならそのまま
    $answer = $number;
  } else {
    # 再帰呼び出し
    $answer = $number * &factorial($number - 1);
  }
  # 結果を戻り値にする
  $answer;
}

最初のfactorialサブルーチンの呼び出しでは、まず、指定値（$number）が1以下かどうかを判定します。そして、1以下のときは、単にその値を代入した変数を戻り値としてサブルーチンを抜け、結果を表示します（!は階乗を表す数学記号です。なお、ここでは0や負の数が指定された場合は考えないことにします）。
指定値が1より大きいときには、計算を行います。この計算の中では、factorialサブルーチン自身を呼び出す「再帰呼び出し」を行っています。2回目のfactorial呼び出しでも、最初の呼び出しと同様に値が1以下かどうかを判定しますが、この呼び出しでは、引数が$numberではなく$number-1であることに注意してください。指定値によってはfactorialサブルーチンが何度も再帰的に呼び出されることになりますが、呼び出しごとに$numberの値から1ずつ減らしているため、呼び出しを永久に繰り返すことはありません。たとえば指定値が4のときには、4×3×2×1を計算した後、「1より大きい」という条件に合わなくなり、すべてのfactorialサブルーチン呼び出しから抜けて、結果を表示します。

■7.2：練習問題
1.	次のスクリプトを実行すると出力される数値はいくつですか。

# 合計を調べる
#  sum.pl

# 初期値
$sum = 10;

# サブルーチン1を呼び出し
&sum1( );

print "合計: $sum\n";

# サブルーチン1
sub sum1 {
  $sum++;
  {
    local($sum);
    $sum++;
    &sum2( );
  }
  &sum3( );
}

# サブルーチン2
sub sum2 {
  $sum++;
}

# サブルーチン3
sub sum3 {
  my($sum);
  $sum++;
}

【答】
11
localとmyで定義した変数は、どちらもサブルーチンを抜けた時点で無効になるので、結局、最初の$sum++だけが意味を持つことになります。

■7.3：練習問題
1.	ハッシュのリファレンスを受け取り、キーと値を逆にして返すサブルーチンを作成し、そのサブルーチンを使用するスクリプトを作りなさい。

ヒント reverse関数の引数にハッシュを指定すると、キーと値を逆にできます。

【答】
# ハッシュのリファレンス
#  reference_hash.pl

%hash = ("A" => 1,
         "B" => 2,
         "C" => 3,
         "D" => 4);

# サブルーチン実行前
print "[実行前]\n";
print "$key = $value\n" while (($key, $value) = each(%hash));

# リファレンスを渡してサブルーチン実行
&sub_reference_hash(\%hash);

# サブルーチン実行後
print "[実行後]\n";
print "$key = $value\n" while (($key, $value) = each(%hash));

# サブルーチン
sub sub_reference_hash {
  # リファレンスを受ける
  my($reference_hash) = @_;

  # リファレンスのハッシュの内容を逆転
  %$reference_hash = reverse (%$reference_hash);
}

2.	次のスクリプトのうち、サブルーチンtriangleの処理を無名サブルーチンを使って書き換えなさい。

# 三角形の面積を調べる
#  triangle.pl

$base = 15;
$height = 20;

$area = &triangle($base, $height);
print "底辺 $base 、高さ $height の三角形の面積は $area です。\n";

sub triangle {
  my($base, $height) = @_;
  my($area) = $base * $height / 2;
  return($area);
}

【答】
# 三角形の面積を調べる (無名サブルーチン)
#  triangle2.pl

$base = 15;
$height = 20;

$triangle_ref = sub ($base, $height) {
                my($base, $height) = @_;
                my($area) = $base * $height / 2;
                return($area);
              };

$area = &$triangle_ref($base, $height);
print "底辺 $base 、高さ $height の三角形の面積は $area です。\n";

■7.4：練習問題
1.	引数に指定した2つの値の商（小数点以下は切り捨て）を返すサブルーチンを含んだモジュールを作りなさい。なお、0で割ろうとしたときには文字列ERRを返すようにしなさい。
【答】
# 2つの数値の商を返す
#  divide.pm

# サブルーチンの定義
sub divide {
  my($number1, $number2) = @_;

  # 割る値が0のときにはエラーを返す
  return("ERR") if ($number2 == 0);

  # 小数点以下は切り捨てて返す
  int($number1 / $number2);
}

1;

	このモジュールは、次のようなスクリプトから呼び出します。

# divide.pmを呼び出す
#  divide_call.pl

# モジュール呼び出し
use divide;

print "割る対象の数値を入力してください: ";
$number1 = <STDIN>;
chomp($number1);
print "割る数値を入力してください: ";
$number2 = <STDIN>;
chomp($number2);

print "商: ", &divide($number1, $number2), "\n";

2.	開始時と終了時に時間を表示するモジュールを作りなさい。なお、このモジュール内では、packageステートメントを使ってtimeというパッケージ名を宣言しなさい。
【答】
# 開始時と終了時に時間を表示する
#  time.pm

# パッケージとして宣言
package time;

# 時間を返す
sub gettime {
  # 時間を取得
  my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday,
    $isdst) = localtime( );
  # 整形して返す
  sprintf("%02d:%02d:%02d", $hour, $min, $sec);
}

# BEGINルーチン
BEGIN {
  print "開始: ", &gettime( ), "\n";
}

# ENDルーチン
END {
  print "終了: ", &gettime( ), "\n";
}

1;

	このモジュールは、次のようなスクリプトから呼び出します。

# timeモジュールを呼び出す
#  time_call.pl

# パッケージ呼び出し
use time;

print "Enterキーを押してください。";
$key = <STDIN>;

	time.pmモジュールでは、時間情報を取得して「時:分:秒」形式で返すgettimeサブルーチンを定義しています。また、BEGINルーチンとENDルーチンを定義して、モジュールの開始時と終了時にgettimeサブルーチンが呼び出されるようにしています。
	time_call.plスクリプトでは、useステートメントでパッケージを呼び出すとともに、表示結果から開始と終了のタイミングを確認しやすく、見やすくするために、［Enter］キーで改行を入力させています。

■7.5：練習問題
1.	Cwdモジュールを使って、現在のディレクトリを表示しなさい。

ヒント Cwdモジュールのcwd関数を使用すると、現在のディレクトリを示す文字列を取得することができます。
【答】
# 現在のディレクトリの名前を表示
#  cwd_call.pl

# モジュール呼び出し
use Cwd;

# 関数呼び出しと表示
print "現在のディレクトリ: ", getcwd( ), "\n";

■8.1：練習問題
1.	コピー元のファイル名とコピー先のファイル名を入力すると、ファイルのコピーを行うスクリプトを作りなさい。
【答】
# ファイルの内容を別のファイル名のファイルにコピーする
#  copy.pl

print "コピー元のファイル名を指定してください: ";
$from_file = <STDIN>;
print "コピー先のファイル名を指定してください: ";
$to_file = <STDIN>;

chomp($from_file, $to_file);

# それぞれのファイルをオープン
open(F, $from_file)   || die "$from_file をオープンできません:$!\n";
open(F2, ">$to_file") || die "$to_file を作成できません:$!\n";

while (<F>) {
  # コピー元のファイルから読み込み、コピー先のファイルに書き出す
  print F2;
}

close(F2);
close(F);

2.	ユーザーが入力した文字を、ファイルdictionary.txtの最後に追加するスクリプトを作成しなさい。
【答】
# 辞書ファイルに追加する
#  dictionary_append.pl

print "追加する文字列を指定してください: ";
$input_str = <STDIN>;

open (F, ">>dictionary.txt") || die "ファイルが開けませんでした:$!\n";
  print F $input_str;
close(F);

3.	指定したファイルを読み取り、ファイルにPerlという文字が含まれているかどうかを調べるスクリプトを作成しなさい。
【答】
# ファイルから文字列を探す
#  find.pl
print "ファイル名を指定してください: ";
$file_name = <STDIN>;
chomp($file_name);

open (F, "<$file_name") || die "ファイルが開けませんでした:$!\n";
  while (<F>) {
    if (/Perl/) {
      $match = 1;
      last;
    }
  }
close(F);

print $match ? "$file_nameには文字列「Perl」が含まれていました。\n" : "$file_nameには文字列「Perl」が含まれていませんでした。\n";

■8.2：練習問題
1.	キーボードの入力を、キーワードENDを含んだ文字列が入るまで、そのまま標準エラー出力に表示するスクリプトを作りなさい。
【答】
# 標準入力を標準エラーにリダイレクトする
#  redirect_stderr.pl

do {
  # 標準入力から入力
  $string = <STDIN>;
  # 標準エラー出力にそのまま出力
  print STDERR $string;
  # ENDを含む文字列があると終了
} until ($string =~ /END/);

2.	1つ目の引数に指定した文字列をファイル名と見なして、そのファイルに標準入力を書き込むスクリプトを作りなさい。なお、引数が指定されていないときには、標準エラー出力にエラーメッセージを表示してスクリプトを終了しなさい。
【答】
# 標準入力をファイルに出力する
#  std_file.pl

if (@ARGV < 1) {
  # 引数が1つも指定されていない
  print STDERR "引数が指定されていません。\n";
  exit(1);
}

# 1つ目の引数をファイル名として作成
open(F, ">$ARGV[0]") || die "$ARGV[0] を作成できません:$!\n";
while (<STDIN>) {
  # 標準入力をファイルハンドルに出力
  print F;
}
close(F);

■8.3：練習問題
1.	指定した引数をファイル名として扱い、そのファイルの所有者がユーザー自身であり、さらにユーザーが読み込み権限を持っていれば、既存の権限に加えて所有者とグループの書き込み権限を付けるスクリプトを作りなさい。

ヒント 既存の権限と書き込み権限0220の論理和をchmodの引数に指定します。

【答】
# 既存のファイルに権限を追加する
#  append_mod.pl

if (@ARGV > 0) {
  if (-o $ARGV[0]) {
    # 所有者のファイルかどうか調べる

    # ファイル情報を取得
    ($device, $inode, $mode, $nlink, $userid, $groupid, 
     $rdev, $size, $accesstime, $modifytime, $changetime, 
     $blocksize, $blockcount)
      = stat($ARGV[0]);

    # 権限を追加
    chmod($mode | 0220, $ARGV[0]);
  } else {
    print "所有のファイルではありません。\n";
  }
} else {
  print "ファイル名を指定してください。\n";
}

2.	ファイル名を引数として受け取り、そのファイルの有無を確認した後、「<ファイル名>を削除しますか? (Y/N):」というメッセージを出して、Yが押されたときのみファイルを削除するスクリプトを作成しなさい。ただし、YとN以外の文字が入力されたときは、もう一度、確認メッセージを表示するものとします。
【答】
# ファイルの削除時に確認する
#  remove_ask.pl

$filename = $ARGV[0];

if (-f $filename) {
  while () {
    print "$filenameを削除しますか? (Y/N): ";
    $answer = <STDIN>;
    chomp($answer);

    if($answer eq 'Y') {
      unlink ($filename) or die "$filenameは削除できませんでした。
        :$!\n";
      last;
    } elsif ($answer eq 'N') {
      last;
    } else {
      print "無効な値が入力されました。\n";
    }
  }
} else {
  print "$filenameは存在しません。\n";
}

■8.4：練習問題
1.	現在の親ディレクトリのファイル一覧を表示するスクリプトを作りなさい。ただし、「.」と「..」は取り除くものとします。
【答】
# 親ディレクトリの内容を読み込み表示する
#  parentdir.pl

# 親ディレクトリをオープンする
opendir(DIR, "..") || die ".. をオープンできません:$!\n";

# 配列に読み込む
@files = readdir(DIR);

# クローズする
closedir(DIR);

# 表示
foreach (@files) {
  # . と .. 以外を表示
  print "$_\n" unless (/\.{1,2}/);
}

	最後のステートメントのunless以下で.と..を除外していることに注意してください。\.は.をエスケープシーケンスにしたもの、{1,2}は直前の文字が1つあるいは2つ続く、という意味でした。

■8.5：練習問題
1.	標準入力に入力した文字列をコマンドとして実行するスクリプトを、system関数を使って作りなさい。
【答】
# system関数による任意のコマンドの実行
#  shell2.pl

print "実行するコマンド名を入力してください: ";
$command = <STDIN>;
chomp($command);

# 指定のコマンドを実行
system($command);

■9.1：練習問題
1.	PINGと入力するとPONGと返事を返す1対1のクライアント/サーバーシステムを作りなさい。
【答】	
サーバー:

# PING/PONGサーバー
#  pingpongserver.pl

# モジュールの使用宣言
use IO::Socket;

# ソケットオブジェクト作成
$server_socket = new IO::Socket::INET (
                        LocalPort => '10000',
                        Proto => 'tcp',
                        Listen => 5,
                        Reuse => 1 );
unless ($server_socket) {
  print "Socket Error:$!\n";
}

# クライアントからの入力
$client_socket = $server_socket->accept( );
$string = <$client_socket>;
if ($string =~ /PING/) {
  # PONGを出力
  print $client_socket "PONG\n";
}

# ソケットを閉じる
$server_socket->close( );

クライアント:

# PING/PONG クライアント
#  pingpongclient.pl

# モジュールの使用宣言
use IO::Socket;

# ソケットオブジェクト作成
$client_socket = new IO::Socket::INET (
                        PeerAddr => 'localhost',
                        PeerPort => '10000',
                        Proto => 'tcp',
                        TimeOut => '5' );
unless ($client_socket) {
  print "Socket Error:$!\n";
}

# 入力をサーバーに送信
$string = <STDIN>;
print $client_socket $string;

# サーバーからの出力を受信して表示
$receive = <$client_socket>;
print $receive;

# ソケットを閉じる
$client_socket->close( );

2.	1.のサーバーを複数のクライアントからの接続を許すように改良しなさい。
【答】
# PING/PONGサーバー
#  pingpongserver2.pl

# モジュールの使用宣言
use IO::Socket;

$SIG{CHLD} = sub { wait( ) };

# ソケットオブジェクト作成
$server_socket = new IO::Socket::INET (
                        LocalPort => '10000',
                        Proto => 'tcp',
                        Listen => 5,
                        Reuse => 1 );
unless ($server_socket) {
  print "Socket Error:$!\n";
}

# クライアントからの入力
do {
  $client_socket = $server_socket->accept( );
  if ($pid = fork( )) {
  }
  elsif (defined $pid) {
    $string = <$client_socket>;
    if ($string =~ /PING/) {
      # PONGを出力
      print $client_socket "PONG\n";
    }
    exit;
  } else {
    die "forkできません。:$!\n";
  }
} until (0);

# ソケットを閉じる
$server_socket->close( );

3.	指定したURLのWebページの更新日付を取得し、記録ファイルweb_record.logにURLと日付をタブで区切った行を追加するスクリプトを作成しなさい。

ヒント GETの代わりにHEADを使うとヘッダ部分だけ取り出すことができます。日付は「Date: 」の後に書かれています。

【答】
# Webページ日付収集スクリプト
#  collect.pl

# モジュールの使用宣言
use IO::Socket;

# URLを入力
print "URLを指定してください: ";
$url = <STDIN>;

# URLをホスト名とファイルパスに分割
chomp($url);
if ($url =~ /http:\/\/([^\/]+)(\/.*)/) {
  $host = $1;
  $file = $2;
} else {
  die "そのURLには対応していません。\n";
}

# 記録ファイルを開く
open(F, ">>web_record.log");

# ソケットオブジェクト作成
$client_socket = new IO::Socket::INET (
                        PeerAddr => $host,
                        PeerPort => 'http',
                        Proto => 'tcp',
                        TimeOut => '5' );
unless ($client_socket) {
  print "Socket Error:$!\n";
}

# 入力をサーバーに送信
print "$host $file" . "\n";
print $client_socket "HEAD $file HTTP/1.0\n\n";
# 出力を取得し、表示(複数行が返ってくるので、取り終わるまでループ)
while ($receive = <$client_socket>) {
  print $receive;
  # 2つ改行文字が入っていることがあるので、ファイルに出力する前に消す
  chomp($receive); chomp($receive);
  if ($receive =~ /^Date: (.*)/) {
    # 日付を取り出し、ファイルに追加
    $date = $1;
    print F "$url\t$date\n";
  }
}

# ソケットを閉じる
$client_socket->close( );
# ファイルハンドルを閉じる
close(F);

■10.1：練習問題
1.	例1のHTMLファイルのそれぞれのタグがどのような意味を持つか調べなさい。
【答】
（省略）

■10.2：練習問題
1.	使用OSを複数選択できるように例2のスクリプトを修正しなさい。
【答】
#!/usr/bin/perl
# アンケート処理CGIプログラムスクリプト(複数値対応)
# ユーザーが使っているOSの合計を調べる
#  questionnaire3.cgi

# モジュール読み込み
use CGI;
$query = new CGI;

# 統計ファイルの存在のチェック
if (-f "data/statistic3.log") {
  # 存在するならファイルをオープンする。失敗したらエラーページを表示
  open(F, "data/statistic3.log") ||
    &error("ファイル data/statistic3.log をオープンできません ($!) 。
           \n");
  while (<F>) {
    # 改行文字を除く
    chomp( );
    # 名前とOS、用途を取得
    ($name, $os, $use) = split(/\t/);
    # 配列@nametotalに名前を蓄積
    push(@nametotal, $name);
    # ハッシュ$ostotalに合計値を蓄積
    # ,で区切られているので分割
    foreach (split(/,/, $os)) {
      $ostotal{$_}++;
    }
    # ハッシュ$usetotalに合計値を蓄積
    # ,で区切られているので分割
    foreach (split(/,/, $use)) {
      $usetotal{$_}++;
    }
  }
  close(F);
}

# アンケートを解析
# 名前からタブ文字を取り除く(ファイルのデータの区切りにしているため)
$name = $query->param('name');
$name =~ s/\t//g;
# 名前に重複がないか解析
foreach (@nametotal) {
  if ($_ eq $name) {
    # 等しいものがあったときにはエラーページを表示
    &error("お名前 $name はすでに登録されています。\n");
  }
}

# リストosに入っているOS名をハッシュに追加
foreach ($query->param('os')) {
  $ostotal{$_}++;
}

# リストuseに入っている用途をハッシュに追加
foreach ($query->param('use')) {
  $usetotal{$_}++;
}

# 記録に追加。失敗したらエラーページを表示
open(F, ">>data/statistic3.log") ||
  &error("ファイル data/statistic3.log に書き込めません ($!) 。\n");
# リストos、useは,で区切った文字列にして書き込み
print F $name, "\t", join(",", $query->param('os')), "\t",
  join(",", $query->param('use')), "\n";
close(F);

# 表示
print $query->header(-charset=>'Shift_JIS'), 
  $query->start_html(-title=>"現在のアンケート結果");
print "<h2>使用OS</h2>\n";
print "<ul>\n";
# 合計数を表示
print "<li><strong>$os</strong>: $count</li>\n"
  while (($os, $count) = each (%ostotal));
print "</ul>\n";

print "<h2>用途</h2>\n";
# 合計数を表示
print "<li><strong>$use</strong>: $count</li>\n"
  while (($use, $count) = each (%usetotal));
print "</ul>\n";

print $query->end_html( );

sub error {
  # エラーページ表示のサブルーチン
  my($message) = @_;

  print $query->header(-charset=>'Shift_JIS'), 
    $query->start_html(-title=>"エラー");
  print "<h1>エラーが発生しました。</h1>\n";
  print $message;
  print $query->end_html( );
  # 異常終了
  exit(1);
}

■10.3：練習問題
1.	例1のスクリプトを改良し、Mozillaというキーワードを含んでいないブラウザと、ホスト名が.jp以外で終わっているホストからのアップロードを禁止しなさい。
【答】
#!/usr/bin/perl
# 制限付きファイル情報表示CGIプログラムスクリプト
#  fileinfo2.cgi

# モジュール読み込み
use CGI;
$query = new CGI;
$filename = $query->param('filename');

# アクセス拒否
if ($query->user_agent( ) !~ /Mozilla/ || 
  $query->remote_host( ) !~ /\.jp$/) {
  &deny( );
}

# 表示
print $query->header(-charset=>'Shift_JIS'), 
  $query->start_html(-title=>"ファイル情報");

print "<h2>ファイル情報</h2>\n";
print "<ul>\n";
# スカラー変数として扱うとファイル名を返す
print "<li>ファイル名:", $filename, "</li>\n";
print "<li>ファイルのMIMEタイプ:",
  $query->uploadInfo($filename)->{'Content-Type'} , "</li>\n";
print "</ul>\n";

print "<h2>ファイル内容</h2>\n";
print "<PRE>\n";
while (<$filename>) {
  # ファイルの内容を表示
  print;
}
print "</PRE>\n";
print $query->end_html( );

sub deny {
  # アクセス拒否
  print $query->header(-charset=>'Shift_JIS'), 
    $query->start_html(-title=>"アクセス拒否");

  # 拒否メッセージ表示
  print "<p>",
  "あなたのホストあるいはブラウザからのアクセスは拒否されました。",
  "</p>\n";
  print $query->end_html( );

  # 異常終了
  exit(0);
}

■11.1：練習問題
1.	CPU、メモリ、ハードディスクなどをノードとしてあなたのコンピュータの構成をXMLで表現しなさい。
【答】
（省略）

2.	あなたが使っているシステムではどのような文書型定義が利用できるか調べなさい。
【答】
（省略）

■11.2：練習問題
1.	例で作成したXMLパーサスクリプトを使って、練習問題11.1で作成したXMLドキュメントを解析しなさい。

2.	XML文字列を受け取って整形式かどうかをparse関数で調べ、整形式に合致していれば「正しいXMLドキュメントです。」とブラウザに返すWeb CGIインターフェイスを作成しなさい。ただし、整形式に合致していないときは、「不正なXMLドキュメントです。」という文字列と、エラーメッセージの両方を返しなさい。

ヒント 長いテキストをCGIで受け付けるために、HTMLのフォームとして<textarea>要素を使うとよいでしょう。<textarea name="xml" cols=60 rows=30></textarea>のように記述します。

【答】
HTMLファイルは次のようになります。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<!--
 XMLドキュメントファイルを受け付けるHTML

 xmlinfo.html
-->
<html>
  <head>
    <title>XMLドキュメントパーサ</title>
  </head>

  <body>
    <h1>XMLドキュメントパーサ</h1>

    <p>解析するXMLドキュメントを記入して［送信］を押してください。</p>

    <form action="/cgi-bin/xmlinfo.cgi" method="post" 
        enctype="multipart/form-data">
      <p><textarea name="xml" cols=60 rows=20></textarea></p>
      <p>
        <input type="submit" name="send" value="送信"><br>
        <input type="reset" value="リセット">
      </p>
    </form>

  </body>
</html>

CGIプログラムは次のようになります。

#!/usr/bin/perl
# XMLパーサCGIプログラムスクリプト
#  xmlinfo.cgi

# モジュール読み込み
use CGI;
use XML::Parser;

$query = new CGI;
$content = $query->param('xml');

# 表示
print $query->header(-charset=>'Shift_JIS'), 
  $query->start_html(-title=>"XML Parser");

$parser = new XML::Parser( ErrorContext=>3 );
eval ( "\$parser->parse( \$content );" );

if ($@) {
  chomp($@);
  $@ =~ s/^\n//;
  $@ =~ s/ at (?:[A-Z]:)?\/.*//;
  $@ =~ s/\&/\&amp;/g;
  $@ =~ s/</\&lt;/g;
  $@ =~ s/>/\&gt;/g;

  print "<p>不正なXMLドキュメントです。</p>\n";
  print "<PRE>\n";
  # エラーメッセージを表示
  print $@;
  print "</PRE>\n";
} else {
  print "<p>正しいXMLドキュメントです。</p>\n";
}
print $query->end_html( );

■11.3：練習問題
1.	次のXMLドキュメントから、name、address、telの要素の値を抜き出し、「名前,住所,電話番号」の形式（CSV形式）で出力を行うスクリプトを作りなさい。

<?xml version="1.0" encoding="utf-8"?>
<addressbook>
  <title>住所録</title>
  <info>
    <name>鈴木</name>
    <address>東京都世田谷区</address>
    <tel>03-XXXX-XXXX</tel>
  </info>
  <info>
    <name>山田</name>
    <address>大阪府大阪市</address>
    <tel>06-XXXX-XXXX</tel>
  </info>
  <info>
    <name>佐藤</name>
    <address>宮城県仙台市</address>
    <tel>022-XXX-XXXX</tel>
  </info>
</addressbook>

【答】
# 住所録からCSVを作成する
#  addressbook.pl
use encoding "utf8", STDOUT=>"Shift_JIS";
use XML::Parser;

# ハンドラ付きでオブジェクトを作成
$parser = new XML::Parser( Handlers => 
				   {Start=>\&element_start,
				    Char => \&cdata,
				    End => \&element_end});

# 要素名保存変数
$element_name = '';
# 住所録情報
%records = {};

# 解析の実行
eval ( "\$parser->parsefile( \@ARGV[0] );");

if ($@) {
  chomp($@);
  $@ =~ s/^\n//;
  $@ =~ s/ at (?:[A-Z]:)?\/.*//;
  print @ARGV[0] . "は不正なXMLドキュメントです (エラー: $@)。\n";
}

sub element_start {
  # 要素の開始
  my ($expat, $name, %attributes) = @_;
  $element_name = $name;
  # ハッシュを初期化
  $records = {} if ( $name eq 'info' );
}

sub element_end {
  # 要素の終了
  my ($expat, $name) = @_;
  if ( $name eq 'info' ) {
    # ハッシュを出力
    print $records{'name'} . "," . $records{'address'} . "," 
      . $records{'tel'} . "\n";
  }
  $element_name = '';
}

sub cdata {
  # テキストデータ
  my ($expat, $context) = @_;
  # ハッシュに格納
  $records{$element_name} = $context if ($element_name);
}

実行結果は次のようになります。

鈴木,東京都世田谷区,03-XXXX-XXXX
山田,大阪府大阪市,06-XXXX-XXXX
佐藤,宮城県仙台市,022-XXX-XXXX

■11.4：練習問題
1.	例11.4 1.のXPathスクリプトを使って、いろいろなパスの表現を試してみなさい。
【答】
（省略）
