プログラミング初心者がつまづいたポイント【Perl編】

## 背景
　とあるネットワーク機器が故障した際の復旧対応方法を再検討する、ということで、
　故障発生時に手動でやっていた最新コンフィグ取得や投入の自動化を試みました。
　Perl言語を選んだ理由は、既に監視系のサーバ環境にインストールされていたためという
　単純な理由です。
　Perlを初めて触りながらの作成だったため、躓いたポイントをまとめました。
　同じように今後初めて触る方に届くことを願って…。

## 使用環境
- Perl 5 ver.5.8.0
- サクラエディタ(デフォルトでPerlに関連する強調等をしてくれます)
## 作成したものについての簡単な概要
- ネットワーク機器に対して、最新のコンフィグを取得してサーバに一時保存
    - 取得したコンフィグの管理IPアドレスを特定のアドレスに変更
- 別の予備ネットワーク機器に対して取得したコンフィグを投入
- 作業結果をメールにて送付
- 上記を毎日特定の時間に実行(cron)
    ![program_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920082/b391587c-5832-c822-56c0-c2d7812ad839.png)


## つまづきポイント
イメージ図の通りに動くプログラムを作るということで作成開始しました。
細かい基本の所は調べれば出てきますし省略して、作成に際して調べてもつまづいた所をまとめます。
作成したプログラムについては、要望があれば載せようかなと思っています。
### プログラムの作成編
#### ①調べたモジュールが標準モジュールとして搭載されてなかった
- **Time::Piece** モジュール
用途としては、プログラム実行時に今日の日付がついたテキストファイルは残して、
昨日の日付がついたテキストファイルを指定して、サーバから削除するという動作のため、
保存するコンフィグのファイル名に日付(yyyymmdd)をつけたかったのです。
ポイントは**昨日の日付**のファイルを削除するということ。
簡単に昨日の日付を出せるモジュールが**Time::Piece**でした。
残念ながら**ver.5.10から**標準モジュールとして搭載で、
使用環境はver.5.8.0で入ってなかったので断念し、別のモジュールを使った力技でなんとかしました。
#### ②Perlにおける真偽が少し特殊
if文は条件が合致(真)であれば{}内のプログラムを実行する、合致しない(偽)であれば
実行しないというのは他のプログラミング言語でも基本ですね。
>if(条件)｛
　実行;
｝

Perlでは偽と判定される値があります。
今回は別の人が作成したプログラムを参考に作成しているのですが、
他人のプログラムを読み解く時に使用されてる場合もあるので、覚えて置いた方が無難かなと思います。
|偽と判定される値   |備考      |
|:------------------|:---------|
|undef              |未定義値  |
|""                 |空文字列  |
|0                  |数字      |
|"0"                |文字列の0 |

#### ③作成したサブルーチンを繰り返し使う場合の配列の宣言や初期化に気を付ける
一つのプログラムの中で、複数の装置にログインを繰り返し実施するため、
装置にログインしセッション数の確認をする等のサブルーチンを作成して
そのサブルーチンを繰り返し呼び出すようにしていました。
その中で配列にコマンド実行結果を入れていたのですが、配列の宣言が**グローバル**になっている状態で使いまわす結果となり、呼び出す度に結果が配列に蓄積されることに。
>例として下記コード内の
・my @OUTPUT_SESSION1 = ();　を記載せずに実行していると、
　TELNET_CONFが呼び出される度に@OUTPUT_SESSION1の中身が蓄積されてしまった。
　**(本来の想定は呼び出される度に初期化されて、@OUTPUT_SESSION1の中身が改めて格納されてほしかった)**
```perl 作成したプログラムの一部.pm
sub TELNET_CONF {
	my $ipadd = $_[0];	# $_[0]第一引数を表す。ホストアドレス格納
	my $host  = $_[1];	# $_[1]第二引数を表す。ファイル名格納
	my $today = $_[2];	# $_[2]第三引数を表す。日付格納
	my @TMP_SHOW;
    my @OUTPUT_SESSION1 = ();　※初期はここの宣言が無し
	my $PROMPT;
	
	## SESSIONコマンドを配列で保存
	my @SESSION_COMMAND = ("show session");
	
	## SHOWコマンドをハッシュで保存
	%SHOW_COMMAND = (
		"runconf" => "show running-config",
	);
    ## Net::Telnet ログイン操作用のサブルーチン呼び出し
	if (&TELNET_LOGIN_DEV($ipadd,"ユーザ名","パスワード")) {	# 別のサブルーチン(TELNET不可なら1が返る⇒真、TELNET成功していたら0が返る⇒偽)
		## ログイン失敗時はサブルーチンから抜ける
		return(1);	## 即座に呼び出し元(xxx.pl)に処理を返す
	}

	## TELNET成功で下記に続く
	## セッションコマンドを投入
	foreach my $sessioncmd (@SESSION_COMMAND) {	# @SESSION_COMMANDを$sessioncmdに格納
		## コマンド実行し結果を配列に保存　
		## cmd - コマンドを発行し、出力を取得。$sessioncmdにoutput_record_separatorが自動的に追加(デフォルトではリターンキー押すのと同義)
		my @TMP_SESSION = $session -> cmd($sessioncmd);
		$PROMPT = pop(@TMP_SESSION);	# コマンド実行後に出てくるプロンプト名の格納 ※pop 配列　⇒配列の最後を取り出す　

		## ログ処理用で別配列に格納　$sessioncmd＝"show session"
		chomp($sessioncmd);						# 改行を削除
        
        ※@OUTPUT_SESSION1 が装置ログインする度に結果を蓄積していくことになってしまった
		push(@OUTPUT_SESSION1,@TMP_SESSION);	# 配列@OUTPUT_SESSION1の最後にshow sessionの結果格納
        
	}
}
```
#### ④実行する前にチェックしましょう
サクラエディタでPerlのプログラムを書く場合、関数や文字列・数値等の色がつき
分かりやすくなっています。
しかしfor等の繰り返し文やif文を組合せる場合にスコープ( **{}**　のこと)の数が
足りていなかったり、変数の型を間違えていたり、セミコロン **;** をつけ忘れていたりと
文法的なミスが起きやすいです。
プログラム実行前に文法チェックできるので、必ず実施しましょう…。
```perl:文法チェック
Perl -c test.pl
```

### プログラムの実行編
初めてプログラムを動かす時は一気に動かすのではなく、
基本的にはプログラムを少しずつ実行して確認した方が良いと思います。
Perlの場合はデバックモードで実行し、特定の文字入力と決定キーで実行・デバックが可能です。
ここでも~~やらかし~~注意すべき点がありましたので記載します。
ちなみにデバックモード実行は以下の通りです。
```perl:デバックモード起動
Perl -d test.pl
```
#### ⑤安易にrは押さない
デバックモードは特定の文字入力と決定キーで様々なことができます。
- **p 変数**
    - プログラムの実行した行時点で、指定した変数の中身を印字します。
    その時点で変数に想定通りの値が入っているかの確認に便利です。
- **n**
    - プログラムを1行実行(サブルーチンは中身の精査を省略して結果のみ出力)します。
- **s**
    - プログラムを1行実行(サブルーチンも1行実行)します。
    こちらは標準モジュールの中も1行ずつ実行していきます。
- **r**
    - サブルーチンから抜け出します。
    sでサブルーチンの中に入ったは良いけど、動きの確認は既に終わっていたりで
はやくサブルーチンから抜け出して結果を確認したい、という時に便利です。

私の場合、標準モジュール(**Net::Telnet**や**Term::ANSIColor**)や何度か繰り返し呼び出しているサブルーチンに**s** で入ってしまった場合、**r** で抜け出して結果を見る、ということをしていました。
しかし、下記の事態もそこそこの頻度で発生しました。
**<font color="Red">「抜け出したくないサブルーチンで抜け出してしまった」
「サブルーチン或いは標準モジュールに入っていると思ってrを押したらプログラム本体(メインルーチン)で最後まで処理が実行されてしまった」</font>**

::: note warn
rを実行する前に、何を実行中か再確認必須
:::

### <font color="MediumBlue">番外：cronでPerlプログラム実行編</font>
「毎日特定の時間に実行」するために、cronで実行させました。
cron の編集(編集方法は vi と同じです。)
```
crontab -e
```
cronの書式は以下の通りです。
　**分(0-59) 時(1-24) 日(1-31) 月(1-12) 曜日(0-7,0=7=日曜日) 実行したいコマンド**

今回はAM6:00に/usr/local/bin/配下にある「config.pl」を実行したかったので、以下の通りに記載。
```:crontab -e
0 6 * * * /usr/local/bin/config.pl
```
#### ⑥cron実行時だけうまく起動しない
サーバ内の同じ場所で、手動でプログラムを実行させるとうまくいくのに、
cronで実行すると動かないというパターンは結構あるそうです。
今回の例でいくと以下の通りです。
`① /usr/local/bin/ に「config.pl」と「telnet.pm」を保存 `
`② 「telnet.pm」は作成したモジュールで、「config.pl」の中で呼び出す`
`③ /usr/local/bin/ で「config.pl」を実行するとうまく動く`
原因としては、②のrequire で指定したモジュール「telnet.pm」の指定方法でした。
```perl:config.pl 改善前
#!/usr/bin/perl
use CGI;
use Jcode; 			#日本語文字コードの変換

## モジュールを実行時に読み込む
require "telnet.pm"; ※同じフォルダにいるのでこの記載だけで良いと思ってました。
```
**相対パスではなく絶対パスで記載が必要とのことでした。**
```perl:config.pl 改善後
#!/usr/bin/perl
use CGI;
use Jcode; 			#日本語文字コードの変換

## モジュールを実行時に読み込む
require "/usr/local/bin/telnet.pm"; ※絶対パスで指定
```
**他にも、プログラムの中でファイルを開く等でパスを指定するような箇所がある場合、
相対パスではなく、絶対パスにする必要があります。**

::: note info
cronで動かす際はプログラム内のパスは絶対パスで記載する
:::

## おわりに
Perlで、実際につまづいた所をあげましたがいかがでしたでしょうか。
本当はもっと細かい所でいろいろとつまづいていましたが、
比較的調べても答えが出てこなさそうor個人的に調べた上でつまづいた所に絞りました。
昔のプログラムを引き継ぐ等でPerl触ることもあるという人はこれからもいるのではないかなと思いますので、少しでも参考になれば良いなと思います。
今後は他の言語も触る予定がありますので、また記事にできたらなと思っています。