PowerShellスクリプトの関数のトラブルシューティング

本記事は 新人ブログマラソン2024 の記事です

はじめまして!2024年入社新人の織田です。

今回業務にて、人生で初めてPowerShellスクリプトを書く機会がございましたので、今回のスクリプト作成にあたって、学んだことのまとめとして本記事を書かせていただこうと思います。
ただ、今回は基礎的な変数の宣言や、値の代入などの話は省かせていただき、今回私がエラーが発生するなどして躓いたポイントのみにフォーカスしたいと思います。
あまり業務でPowerShellスクリプトを書く機会は多くないかもしれませんが、同様の問題に遭遇した際に解決の一助になれば幸いです。

それでは、始めていきましょう!

 動作環境

今回作成したスクリプトの動作環境は下記の通りとなります。

項目
実行環境OS Windows Server 2016 Datacenter
PowerShell version 5.1
2024年現在、PowerShellの最新バージョンは7.5があり、動作環境によってはバージョンの差による違いもあるかもしれません。
こちらにMicrosoft公式がまとめた「Windows PowerShell 5.1 と PowerShell 7.x の相違点」のページがあります。
バージョン7.xを使用中の方はこちらも併せてご確認ください。

遭遇した問題と解決策

ここでは今回のスクリプト作成にあたり、遭遇した問題とその解決策についてまとめていきたいと思います。
章のタイトルを「エラー」ではなく「問題」としているのは、PowerShellではエラーとして検出されないものもあったためです。
ただ、私のこれまでの経験によって発生した問題なども含まれるため、参考にならない情報も多く含まれているかもしれない点はご了承ください。

今回作成したスクリプトは繰り返しの記述が多く、それらをできるだけなくすために今回は関数を使用しました。
その際、私が遭遇した問題点は下記の3つです。

  • 関数の定義と関数の呼び出しの順番
  • 関数呼び出し時の引数の渡し方
  • 関数名の設定

それぞれについて、詳しく説明していきたいと思います。

関数の定義と呼び出しの順番

PowerShellスクリプト作成中、繰り返しの処理を関数としてまとめようとした際に遭遇した問題です。
一言で言えば、関数の呼び出しの記述の前に関数を定義しておく必要があるということです。

■誤ったスクリプト

func "test"

function func($a) {
    Write-Host $a
}
■エラーメッセージ
func : 用語 ‘func’ は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。
発生場所 C:\Users\user-name\test.ps1:1 文字:1

+ func “test”
+ ~~~~
+ CategoryInfo : ObjectNotFound: (func:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
※上記のエラーは「func」という名前の関数を定義される前に呼び出した場合のものになります。
正しくは下記の通り、関数を定義してから呼び出しを行う必要があります。
■スクリプト
function func($a) {
    Write-Host $a
}

func "test"
■実行結果
PS C:\Users\user-name> .\test.ps1
test
まとめると、
原因:関数を定義する前に呼び出していた。
解決策:関数は定義後に呼び出す必要がある。
となります。

関数呼び出し時の引数の渡し方

PowerShellにおける関数の特徴として、定義した関数を呼び出す際の引数の渡し方があります。
PowerShellでは関数を呼び出す際に、「関数名(引数1, 引数2)関数名 値1, 値2ではなく、「関数名 引数1 引数2というように呼び出します。
この勘違いの厄介なところは、エラーが出ないということです。
より正確には、関数定義時に引数を必須しない場合、値が渡されなかった引数には「$null」が自動で入ります。
また、PowerShellでは「(値1, 値2)」や「値1, 値2」という書き方は配列としても認識されるため、上記の誤った呼び出しを行った場合、一つ目の引数に2つの値が配列として代入され、二つ目の引数に「$null」が代入されることになります

例として、次のスクリプトの実行結果を挙げます。

■スクリプト

function func($a, $b) {

    Write-Host $a
    Write-Host $b

    if($b -eq $null) {
        Write-Host "`$b is null."
    } else { 
        Write-Host "`$b is not null." 
    }
}

func ("test1", "test2")
Write-Host "---------------------"
func "test3", "test4"
Write-Host "---------------------"
func "test5" "test6"

■実行結果

PS C:\Users\user-name> .\test.ps1
test1 test2

$b is null.
---------------------
test3 test4

$b is null.
---------------------
test5
test6
$b is not null.

となります。実際に、「func (“test1”, “test2”)」や「func “test3”, “test4”」という引数の渡し方をしたものは、2つ目の引数に値が入っていないことがわかります。

まとめると、
原因:関数を間違った呼び出し方をしてしまっていた。
解決策:原則関数を呼び出す際の引数の書き方は関数名 引数1 引数2 …」とする。
となります。

関数名の設定

一般的にプログラミングをしていると予約語というものに遭遇することがあると思います。
PowerShellにも、予約語は存在しており、変数名や関数名を設定する際には、それを避ける必要があります。
ただ、PowerShellの特徴は関数名を設定する際に、予約語以外にも注意する必要があるということです。
関数名を設定する際に追加で気を付ける必要があるものは、既存のPowerShellコマンドです。
通常、プログラミング言語でコーディングしている際には、予約語以外の既存関数などはインポートなどをしない場合、名前の衝突を気にする必要はありません。
しかし、PowerShellスクリプトやそのほかのシェルスクリプトでは、パスの通っているコマンドがスクリプト中で呼び出すことが可能であり、PowerShellの場合は特に関数の呼び出し方とコマンドの呼び出し方が同じです
そのため、自分で新しく定義したはずの関数が既存のコマンドとして実行され、スクリプトが意図しない処理を実行してしまう恐れがあります。
■誤ったスクリプト
function move($a, $b) {
    Write-Host "Move", $a, "to", $b
}
move "test" "test2"
■エラーメッセージ
move : パス ‘C:\Users\user-name\test’ が存在しないため検出できません。
発生場所 C:\Users\user-name\test.ps1:4 文字:1
+ move “test” “test2”
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\user-name\test:String) [Move-Item], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.MoveItemCommand
このエラーはPowerShellに「move」に該当するコマンドがすでに存在するため、定義したmove関数が実行されず、別のmoveコマンドが実行されましたが、引数で指定されたフォルダが存在しないため発生しています。
回避策としては、同名のコマンドが存在しないことを調べてから関数名を設定する方法が有効です。
具体的には下記のコマンドを使用します。
gcm <コマンド名>
gcmGet-Commandコマンドの略になります
実際にgcmで「move」を調べてみると下記のような結果になります。
PS C:\Users\user-name> gcm move

CommandType Name              Version Source
----------- ----              ------- ------
Alias       move -> Move-Item
つまり、Move-ItemコマンドのAliasとして「move」が使用されているということです。
代わりの関数名として、例えば「move-alpha」を考えたとして、gcmで確認すると、「move-alpha」に該当するコマンドは存在していないため、
PS C:\Users\user-name> gcm move-alpha
gcm : 用語 'move-alpha' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。
発生場所 行:1 文字:1
+ gcm move-alpha
+ ~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (move-alpha:String) [Get-Command], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand
という結果になります。
そのため、スクリプトにて「move」を「move-alpha」に変更すると、自分が定義した関数である、move-alphaが実行できます。
■スクリプト
function move-alpha($a, $b) {
    Write-Host "Move", $a, "to", $b
}
move-alpha "test" "test2"
■実行結果
PS C:\Users\user-name> .\test.ps1 
Move test to test2
まとめると、
定義した関数名と同名のコマンドが存在する場合、コマンドの実行が優先される
Get-Commandによって同名のコマンドが存在しないことを確認してから関数名を設定する
となります。

まとめ

以上で今回私が遭遇した関数関係の問題は終わりです。
そのほか、PowerShellにおける関数の詳細な仕様を知りたい方は、下記参考サイトをご参照ください。
参考サイト:about_Functions – PowerShell | Microsoft Learn(関数)

 

おまけ

ここでは今回のスクリプト作成にあたり、有益だと感じた要素をまとめます。

ログの保存

業務に関する処理はログを残しておき、あとから処理が正しく実行されていたかを確認する必要があります。
Start-TranscriptStop-Transcirptを使用することで、その間にPowerShellへ出力されたテキスト情報をファイルに保存することができます。

■スクリプト

Start-Transcript

Write-Host "a"
Write-Host "b"
Write-Host "c"
Write-Host "d"

Stop-Transcript

■実行結果

PS C:\Users\user-name> .\test.ps1
トランスクリプトが開始されました。出力ファイル: C:\Users\user-name\Documents\PowerShell_transcript.device-name.b4xgbacx.20241224150409.txt
a
b
c
d
トランスクリプトが停止されました。出力ファイル: C:\Users\user-name\Documents\PowerShell_transcript.device-name.b4xgbacx.20241224150409.txt

※今回はトランスクリプトの出力パスとファイル名を指定していないため、実行ユーザーのドキュメントフォルダに「PowerShell_transcript.<デバイス名>.<ランダムな文字列>.<実行日時(yyyymmddHHMMSS)>.txt」という名前でファイルが生成されています。

■ログファイルの中身

**********************
Windows PowerShell トランスクリプト開始
開始時刻: 20241224150409
ユーザー名: device-name\user-name
RunAs ユーザー: device-name\user-name
構成名: 
コンピューター: device-name (Microsoft Windows NT 10.0.22631.0)
ホスト アプリケーション: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -Command Import-Module 'c:\Users\user-name\.vscode\extensions\ms-vscode.powershell-2024.4.0\modules\PowerShellEditorServices\PowerShellEditorServices.psd1'; Start-EditorServices -HostName 'Visual Studio Code Host' -HostProfileId 'Microsoft.VSCode' -HostVersion '2024.4.0' -BundledModulesPath 'c:\Users\user-name\.vscode\extensions\ms-vscode.powershell-2024.4.0\modules' -EnableConsoleRepl -StartupBanner "PowerShell Extension v2024.4.0
Copyright (c) Microsoft Corporation.

`https://aka.ms/vscode-powershell
Type 'help' to get help.
" -LogLevel 'Normal' -LogPath 'c:\Users\user-name\AppData\Roaming\Code\User\globalStorage\ms-vscode.powershell\logs\1735016531-a08696d6-c03f-44f0-884a-6074e122f4c11735016528163' -SessionDetailsPath 'c:\Users\user-name\AppData\Roaming\Code\User\globalStorage\ms-vscode.powershell\sessions\PSES-VSCode-21068-641835.json' -FeatureFlags @() 
プロセス ID: 22968
PSVersion: 5.1.22621.4391
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.22621.4391
BuildVersion: 10.0.22621.4391
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
トランスクリプトが開始されました。出力ファイル: C:\Users\user-name\Documents\PowerShell_transcript.device-name.b4xgbacx.20241224150409.txt
a
b
c
d
**********************
Windows PowerShell トランスクリプト終了
終了時刻: 20241224150409
**********************

※今回VSCode中で実行しているため、関連した出力が含まれています。

参考サイト1:Start-Transcript (Microsoft.PowerShell.Host) – PowerShell | Microsoft Learn
参考サイト2:Stop-Transcript (Microsoft.PowerShell.Host) – PowerShell | Microsoft Learn

 

サマリの出力

処理の内容をサマリとしてまとめて簡単に把握できるようにすると結果を確認するとき非常に便利です。
実現するには、

$Data = New-Object PSObject | Select-Object Data1, Data2, ...
$Data.Data1 = "text1"
$Data.Data2 = "text2"
...
$Summary += $Data

という書き方をし、配列として格納したのち、

$Summary | Format-Table

とすることで、テーブル形式でデータを表示することができます。

■スクリプト

$Summary = @()

$PrimeMinisters = (("Ishida", "Shigeo", 86),
("Kishimoto", "Fumi", 1095),
("Sugawara", "Yoshio", 385))

foreach ($PrimeMinister in $PrimeMinisters) {
    $Data = New-Object PSObject | Select-Object FirstName, LastName, Days
    $Data.FirstName = $PrimeMinister[1]
    $Data.LastName = $PrimeMinister[0]
    $Data.Days = $PrimeMinister[2]
    $Summary += $Data
}

$Summary | Format-Table

■実行結果

PS C:\Users\user-name> .\test.ps1

FirstName LastName  Days
--------- --------  ----
Shigeo    Ishida    86
Fumi      Kishimoto 1095
Yoshio    Sugawara  385

参考サイト1:New-Object (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Learn
参考サイト2:Select-Object (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Learn
参考サイト3:Format-Table (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Learn

タイトルとURLをコピーしました