PowerShell スクリプトモジュール(.psm1 ファイル)から 参照元の PowerShell スクリプト(.ps1 ファイル)の名前を取得するのに苦労したというお話です。
苦労したところは置いておいて、結論を知りたいという方は最後の【まとめ】をご覧ください。
背景
PowerShell で複数のスクリプト(.ps1)を連動させて実現する仕組みを 構築する機会があり、役割や機能ごとに.ps1を作成していました。
その際、例えば、デバッグログを出力するためのクラス・関数等、 どの.ps1でも同じことを行う処理も当然あり そういったものは、各.ps1から共通で使えるように スクリプトモジュール(.psm1)としてライブラリ化する という方法をとることもあるかと思います。
そうなったときに「参照元の.ps1ごとにデバッグログを分けてファイル出力したい」等、 参照元の.ps1のファイル名を取得したい、というケースが出てきます。
参照元の.ps1ファイルから、共通モジュールを呼び出す際に自分のファイル名を渡すことでも もちろん実現はできるのですが、あまりスマートではないな、と思い 参照元の処理に依らずに実現できないか、と調べて(試して)みました。
検証内容
今流行りの生成AIも活用しつつ方法を模索したのですが 提示された方法を試しても「参照元の処理に依らずに実現する」という目的に合致しないものも多く、 なかなかこれ、という解が得られません。 何度も押し問答を行った結果、たどり着いた方法が下記の2つです。
- Get-PSCallStack の結果から取得する
- .ps1 実行時のコマンドラインの情報から取得する
それぞれの詳細は下記の通りです。
Get-PSCallStack の結果から取得する
下記内容の、スクリプトおよび、スクリプトモジュールを用意します。 ※ExecutionPolicy の関係でバッチファイル経由で実行しています。
run1.bat
@echo off
cd %~dp0
powershell -NoProfile -ExecutionPolicy RemoteSigned .\script1-1.ps1
script1-1.ps1
# モジュール読み込み
using module "./module1.psm1"
module1.psm1
# コールスタック表示
(Get-PSCallStack | Format-Table Command, Position) | Out-File output.txt -Encoding Default
run.bat を実行すると output.txt へ下記が出力されます。
output.txt
Command Position
------- --------
module1.psm1 (Get-PSCallStack | Format-Table Command, Position) | Out-File output.txt -Encoding Default
<ScriptBlock> .\script1-1.ps1
最後の行に「script1-1.ps1」が見えますね! (Get-PSCallStack)[-1].Position.Text から取得できそうです。
.psm1 を手直しして
module1.psm1
# .ps1ファイル名取得
$ps1file = (Get-PSCallStack)[-1].Position.Text
# ファイルに出力して確認
$ps1file | Out-File output.txt -Encoding Default
run1.bat を実行すると output.txt へ下記が出力されます。
output.txt
.\script1-1.ps1
取れました。
しめしめと、これでコーディングを進めていたのですが、 別のスクリプトで同じようにモジュールを組み込むと なぜか.ps1ファイルの名前が取得できていないことがありました。 よくよく確認すると、Get-PSCallStack の結果が下記のようになっています。
output.txt
Command Position
------- --------
module1.psm1 (Get-PSCallStack | Format-Table Command, Position) | Out-File output.txt -Encoding Default
肝心の最後の行が出力されなくなっています。
この結果の違いは何だろう、と試行錯誤したところ、.ps1 の呼び出し方に違いがあることに気づきました。
.ps1 のファイル名が取得できる場合は、バッチファイルから下記のように呼び出しています。
run1.bat(抜粋)
powershell -NoProfile -ExecutionPolicy RemoteSigned .\script1-1.ps1
コールスタックの最後の行が出力されないスクリプトは、別の.ps1から下記のように呼び出していました。
script1-2.ps1(抜粋)
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File .\script1-3.ps1" -WorkingDirectory (Get-Location) -PassThru -Wait
.bat と.ps1なのでそもそもコーディングが違いますが、呼び方の違いがお判りでしょうか…? ※ .ps1から呼び出したらダメ、ということではありません。
・ ・ ・ ・
正解は…
-ArgumentList "-NoProfile -File .\script1-3.ps1"
↑↑↑ここの部分
コールスタックの最後の行が出力されないときには、powershell.exe のパラメータに「-File」オプションを指定しています。 冒頭の例に挙げた run1.bat では -File は省略して、実行するスクリプトファイル名を記述していました。 試しに、バッチファイルでの呼び出しにも -File を付けると同じようにコールスタックの最後の行が出力されなくなりました。 とても謎な挙動です…。
目的である「参照元の処理に依らずに実現する」に対して若干思惑通りにはなりませんでしたが もしこの方法を試される場合はご注意くださいませ。
.ps1 実行時のコマンドラインの情報から取得する
もう一つの実現方法です。 こちらの方がコーディングの量は増えますが、直感的にはわかりやすいかと思います。
下記内容の、スクリプトおよび、スクリプトモジュールを用意します。 ※ExecutionPolicy の関係でバッチファイル経由で実行しています。
run2.bat
@echo off
cd %~dp0
powershell -NoProfile -ExecutionPolicy RemoteSigned .\script2.ps1
script2.ps1
# モジュール読み込み
using module "./module2.psm1"
module2.psm1
# コマンドライン表示
[System.Environment]::CommandLine | Out-File output.txt -Encoding Default
run2.bat を実行すると output.txt へ下記が出力されます。
output.txt
powershell -NoProfile -ExecutionPolicy RemoteSigned .\script2.ps1
.ps1 のファイル名を含んでいるので、あとはこれを分解すればよさそうです。
.psm1 を手直しして
module.psm1
$ps1file = ""
# コマンドラインを取得・半角スペースで分割
$cmdline = [System.Environment]::CommandLine -Split " "
# 分割した要素から、".ps1"を含むものを探す
foreach ($elm in $cmdline) {
$elm = $elm -replace '"', ''
if ($elm -match "\.ps1$") {
$ps1file = $elm
break
}
}
# ファイルに出力して確認
$ps1file | Out-File output.txt -Encoding Default
run2.bat を実行すると output.txt へ下記が出力されます。
output.txt
.\script2.ps1
取れました。
まとめ
まとめると実現方法としては下記2点。 絶対パスで指定されている場合も考慮して、最後の「\」より後だけを切り取る処理も入れています。
Get-PSCallStack の結果から取得する
# .ps1ファイル名取得
$ps1file = ((Get-PSCallStack)[-1].Position.Text -Split "\\")[-1]
# ファイルに出力して確認
$ps1file | Out-File output.txt -Encoding Default
※スクリプト実行時、powershell.exe のパラメータの「-File」は省略する必要がある
.ps1 実行時のコマンドラインの情報から取得する
$ps1file = ""
# コマンドラインを取得・半角スペースで分割
$cmdline = [System.Environment]::CommandLine -Split " "
# 分割した要素から、".ps1"で終わるものを探す
foreach ($elm in $cmdline) {
$elm = $elm -replace '"', ''
if ($elm -match "\.ps1$") {
$ps1file = ($elm -Split "\\")[-1]
break
}
}
# ファイルに出力して確認
$ps1file | Out-File output.txt -Encoding Default
以上、何かしらの参考になれば幸いです。