本記事は 新人ブログマラソン2024 の記事です。 |
はじめまして!2024年入社新人の織田です。
今回業務にて、人生で初めてPowerShellスクリプトを書く機会がございましたので、今回のスクリプト作成にあたって、学んだことのまとめとして本記事を書かせていただこうと思います。
ただ、今回は基礎的な変数の宣言や、値の代入などの話は省かせていただき、今回私がエラーが発生するなどして躓いたポイントのみにフォーカスしたいと思います。
あまり業務でPowerShellスクリプトを書く機会は多くないかもしれませんが、同様の問題に遭遇した際に解決の一助になれば幸いです。
それでは、始めていきましょう!
動作環境
今回作成したスクリプトの動作環境は下記の通りとなります。
項目 | 値 |
実行環境OS | Windows Server 2016 Datacenter |
PowerShell version | 5.1 |
遭遇した問題と解決策
ここでは今回のスクリプト作成にあたり、遭遇した問題とその解決策についてまとめていきたいと思います。
章のタイトルを「エラー」ではなく「問題」としているのは、PowerShellではエラーとして検出されないものもあったためです。
ただ、私のこれまでの経験によって発生した問題なども含まれるため、参考にならない情報も多く含まれているかもしれない点はご了承ください。
今回作成したスクリプトは繰り返しの記述が多く、それらをできるだけなくすために今回は関数を使用しました。
その際、私が遭遇した問題点は下記の3つです。
- 関数の定義と関数の呼び出しの順番
- 関数呼び出し時の引数の渡し方
- 関数名の設定
それぞれについて、詳しく説明していきたいと思います。
関数の定義と呼び出しの順番
PowerShellスクリプト作成中、繰り返しの処理を関数としてまとめようとした際に遭遇した問題です。
一言で言えば、関数の呼び出しの記述の前に関数を定義しておく必要があるということです。
■誤ったスクリプト
func "test" function func($a) { Write-Host $a }
発生場所 C:\Users\user-name\test.ps1:1 文字:1
+ func “test”
+ ~~~~
+ CategoryInfo : ObjectNotFound: (func:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
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つ目の引数に値が入っていないことがわかります。
関数名の設定
PowerShellにも、予約語は存在しており、変数名や関数名を設定する際には、それを避ける必要があります。
ただ、PowerShellの特徴は関数名を設定する際に、予約語以外にも注意する必要があるということです。
関数名を設定する際に追加で気を付ける必要があるものは、既存のPowerShellコマンドです。
通常、プログラミング言語でコーディングしている際には、予約語以外の既存関数などはインポートなどをしない場合、名前の衝突を気にする必要はありません。
しかし、PowerShellスクリプトやそのほかのシェルスクリプトでは、パスの通っているコマンドがスクリプト中で呼び出すことが可能であり、PowerShellの場合は特に関数の呼び出し方とコマンドの呼び出し方が同じです。
そのため、自分で新しく定義したはずの関数が既存のコマンドとして実行され、スクリプトが意図しない処理を実行してしまう恐れがあります。
function move($a, $b) { Write-Host "Move", $a, "to", $b } move "test" "test2"
発生場所 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
具体的には下記のコマンドを使用します。
gcm <コマンド名>
実際にgcmで「move」を調べてみると下記のような結果になります。
PS C:\Users\user-name> gcm move CommandType Name Version Source ----------- ---- ------- ------ Alias move -> Move-Item
代わりの関数名として、例えば「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
参考サイト2:Get-Command (Microsoft.PowerShell.Core) – PowerShell | Microsoft Learn
まとめ
以上で今回私が遭遇した関数関係の問題は終わりです。
そのほか、PowerShellにおける関数の詳細な仕様を知りたい方は、下記参考サイトをご参照ください。
参考サイト:about_Functions – PowerShell | Microsoft Learn(関数)
おまけ
ここでは今回のスクリプト作成にあたり、有益だと感じた要素をまとめます。
ログの保存
業務に関する処理はログを残しておき、あとから処理が正しく実行されていたかを確認する必要があります。
Start-TranscriptとStop-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