先日、適当に作成したPowerShellスクリプトが予期せぬ動作をし、混乱しました。
原因は関数の宣言前にその関数を呼び出したからという、とても単純なことでしたが、原因特定に時間がかかったため、その詳細と原因を記録しておきます。
巻き上げ(Hoisting、ホイスティング)について
事象の説明の前に、巻き上げについて簡単に説明します。
巻き上げとは、JavaScriptなどで見られる、宣言がスコープの先頭に移動する挙動です。
console.log(main());
// ↑この時点ではmainは定義されてないが、
// 巻き上げによって宣言がスコープの先頭に移動するため実行できる。
function main() {
return "hello!";
}
大雑把にいうと、上記のように、宣言がスコープの先頭に移動することを巻き上げといいます。
JavaScriptにはこの仕様がありますが、他の言語では一般的ではないと思います。
私はこの巻き上げがPowerShellでも起こると誤解し、そのために予期せぬ事象に遭遇しました。
発生した事象1:意図せず「マウスのプロパティ」が開く
先日、大量のファイルから特定の条件に合致する部分を検索する処理を、PowerShellで自動化しようとしました(利用可能な環境がPowerShellのみだったため)。
PowerShell ISEで以下のコードを作成しました。
main # 宣言前に呼び出しているので誤り
function main { # 関数名は適当にmainにした
"(中略)"
}
直前までJavaScriptを書いていたため、PowerShellにも巻き上げがあると錯覚し、main
関数の宣言前に呼び出してしまいました。
また、このスクリプトは一時的な利用を想定しており、関数名は深く考えずmain
としました。
それが後に混乱を招く最大の要因になるとも知らずに。
main
関数の処理(中略部分)は、とりあえず対象ファイル名(例:xxx.json
、yyy.json
)をコンソールに出力するだけの簡単なものでした。
実行すると、次の画面が出ました。
予想外すぎるなぁ……
「フォーカスが変な場所に移動し、誤って起動したのか?」と考え、再度実行すると、今度は意図通りに動作しました。
発生した事象2:PowerShell ISEでの修正が反映されない
PS C:\Users\xxx\Documents> test.ps1
xxx.json
yyy.json
コンソールにファイル名が表示されたことを確認し、「ファイルごとの繰り返しの部分は問題なさそうだ」と思いました。
次に、本来の目的である検索処理を実装し、再度実行しました。
しかし、修正が反映されず、依然としてファイル名を表示するだけの古い処理が実行されました。
5回くらい保存して再実行しても、修正は反映されませんでした。
(5回も保存したのに!)
「PowerShell ISEを再起動すれば反映されるかもしれない」と考え、ISEを再起動して実行したところ、またも以下の画面が表示されました。
勘弁してくれ……
事象1の原因:main.cpl
が呼び出されているため
「マウスのプロパティ」が開いたり、修正が反映されなかったりといった予期せぬ挙動の原因は、単純なものでした。
まず、「巻き上げがある」という誤った前提でコードを作成したことです。
main
関数の宣言前に呼び出しているため、本来は誤った実装です。
この誤りに気づかなかったのは、以下のエラーが表示されなかったためです。
main : 用語 'main' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合は
そのパスが正しいことを確認してから、再試行してください。
このエラーが出なかったため、main
の呼び出しに問題がないと誤認していました。
しかし調査の結果、宣言前のmain
は実際にはmain.cpl
というファイルを指しており、これが「マウスのプロパティ」だったのです。
(main.cpl
という名称の理由は調べても分かりませんでした)
再実行で想定通り動いたのは、main.cpl
実行後、main
関数の宣言が読み込まれるためです。
PowerShell ISEでは、実行完了後も変数や関数の宣言が保持されるため、2回目の実行では1回目の実行時に定義されたmain
関数が参照され、正常に動作したように見えたのです。
これは、以下のコードを連続して実行したのと同じです。
# 1回目
main # main.cplが呼ばれる(「マウスのプロパティ」が開く)
function main { # mainを上書き
"(中略)"
}
# 2回目
main # 上で宣言したmainが呼ばれる
function main { # 宣言されるが、これは呼ばれない
"(中略)"
}
まとめ
- 事象:初回実行時のみ「マウスのプロパティ」が開く
-
原因:
- PowerShellにも巻き上げがあると錯覚したこと
-
main
宣言前のmain
呼び出しによる、main.cpl
の実行 -
main.cpl
が「マウスのプロパティ」であることをしらなかったこと - 間違っていたらエラーに出るだろうと思って適当にコーディングしたこと
事象2の原因:実行を途中で打ち切っているため
コード修正後の内容が反映されなかった原因も単純で、「実行を途中で打ち切っていた」ためです。
当初、ファイル名表示処理で基本的な動作を確認した後、検索処理を実装しました。しかし、宣言前にmain
を呼び出しているため、修正前の古いmain
関数が実行されていました。
ファイル数が多かったため処理に時間がかかり、修正が反映されていないことに気づいた私は、実行を途中で停止し、再度保存してから実行しました。
しかし、処理がmain
関数の宣言まで到達しなかったため、古い定義が参照され続け、修正は反映されませんでした。
時系列順に見ると、以下のようになります。
# 1回目の実行
main # main.cplが呼ばれる(「マウスのプロパティ」が開く)
function main { # ファイル名表示処理のmain関数を定義
"(各ファイルについて、ファイル名を表示)"
}
# 2回目の実行
main # 定義済みのファイル名表示処理のmain関数が実行される。
function main {
"(各ファイルについて、ファイル名を表示)"
}
# ここでmain関数の処理を検索処理に修正
# 3回目の実行
main # 定義済みのファイル名表示処理のmain関数が実行される。
# 修正されていないことに気づき、途中で実行を停止する。
function main { # 実行が停止されたため、この宣言は読み込まれない
"(各ファイルについて、条件に合う部分を検索)"
}
# 4回目の実行
main # 定義済みのファイル名表示処理のmain関数が実行される。
# やはり修正されていないことに気づき、途中で実行を停止する。
function main { # 実行が停止されたため、この宣言は読み込まれない。
"(各ファイルについて、条件に合う部分を検索)"
}
まとめ
- 事象:PowerShell ISEでコードを修正・保存しても実行に反映されない
-
原因:
- 宣言前の
main
呼び出しと、実行の途中終了 - 実行後も関数の定義が保持されていることを意識していないこと
- 宣言前の
まとめ
振り返れば「宣言前に呼び出すな」という単純な話でしたが、当時は原因が分からず混乱しました。
実際にはコードがもう少し複雑だったため、問題箇所の特定に時間を要しました。
(もしかしたら、コードをAIに渡して指摘してもらえば簡単だったかもしれませんが、そうしなかったのは「原因は自分で見つけられるはずだ」という、しょうもないプライドがあったのからなのかもしれません)