本記事は PowerShell Advent Calendar 2019 の 2 日目の記事です。
12月3日の0時を過ぎてから書いてます。すまん。
qiita.com
言わずもがなですが、PowerShell は処理の自動化を得意とした言語です。
そして、自動処理において、エラーへの対処は重要です。
何十万件というデータを処理してしまってから、実はエラーがあって、すべての処理結果が壊れているなんていうことになったら、目も当てられません。
ですから、(一般論としては)エラーが起きたら、可及的速やかに処理を中断し、報告すべきです。
別に何も目新しい話ではないのですが、自分でも時々「あれ、どうだったっけ?」と思うことがあるので、そのメモ書きです。
なお、以下、特記しない限り、PowerShell Core 6.2.3 + VSCode で検証しています。
中断されるエラーと中断されないエラー
以下のようなスクリプトを実行してみましょう。どうなるでしょうか。
Get-Item -Path 'no-such-item' Write-Host 'Hello' throw [System.Exception]::new('Oops!!') Write-Host 'World'
結果はこんな感じになります。
Get-Item : Cannot find path 'C:\Users\aetos\no-such-item' because it does not exist. At C:\Users\aetos\test.ps1:1 char:1 + Get-Item -Path 'no-such-item' + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (C:\Users\aetos\no-such-item:String) [Get-Item], ItemNotFoundException + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand Hello Oops!! At C:\Users\aetos\test.ps1:5 char:1 + throw [System.Exception]::new("Oops!!") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : OperationStopped: (:) [], Exception + FullyQualifiedErrorId : Oops!!
コンソールには「Hello」は出ますが「World」は出ません。
つまり、Get-Item コマンドに存在しないパスを渡しても処理は中断されませんが、Exception を throw すると処理は中断されます。
PowerShell にはこのように、コンソールに赤文字で表示されるエラーが出ていても、中断される状況とされない状況があります。
ErrorAction と ErrorActionPreference
Get-Item のところを、このように書き換えると、存在しないパスを渡した時にも中断されます。
Get-Item -Path 'no-such-item' -ErrorAction Stop
コマンド パラメーターの ErrorAction に Stop を指定するということは、そのコマンドの実行中だけ、特別な変数 $ErrorActionPreference に Stop を指定することと同義です。
ですから、以下のように書いても、Get-Item のエラーで止まるようになります。
$ErrorActionPreference = 'Stop' Get-Item -Path 'no-such-item'
こちらの場合、$ErrorActionPreference の値を元に戻さなければ、Get-Item の後でもずっとそのままです。
安全側に倒したい自動処理スクリプトでは、こちらの方が向いていることもあるかもしれません。
なお、$ErrorActionPreference の型は ActionPreference 列挙型で、既定の「エラーメッセージを出して処理継続」は Continue です。
Write-Error
Write-Error コマンドは、コンソールに赤文字でエラー メッセージを出すだけではなく、実際に WriteErrorException という例外を発生させます。
つまり、Write-Error コマンドの ErrorAction パラメーターに Stop を指定したり、Write-Error コマンドの実行前に $ErrorActionPreference に Stop を指定したりしておけば、それ以上処理が進むことはありません。
try-catch
エラー処理と言えば try-catch ですね。
こう書いたらどうなるでしょうか。
$ErrorActionPreference = 'Continue' Write-Host 'Hello' try { Write-Error 'Oops!!' } catch { Write-Host 'Error!!' } Write-Host 'World'
結果はこうなります。
Hello C:\Users\aetos\test.ps1 : Oops!! + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,caller1.ps1 World
「Error!!」は表示されていません。つまり、try-catch ではエラーを捕まえられていません。
ErrorAction パラメーターを設定すると挙動が変わります。
$ErrorActionPreference = 'Continue' Write-Host 'Hello' try { Write-Error 'Oops!!' -ErrorAction Stop } catch { Write-Host 'Error!!' } Write-Host 'World'
この場合の出力はこうなります。
Hello Error!! World
つまり、try-catch で捕まえられるのは、前述した「中断されるエラー」だけだということです。
他のスクリプトを呼び出す場合
こんなスクリプトだとどうでしょうか。
# caller.ps1 $ErrorActionPreference = 'Continue' Write-Host 'Hello' & .\callee.ps1 Write-Host 'World'
# callee.ps1 Write-Error 'Oops!!' -ErrorAction Stop
コンソールには「World」は出力されません。つまり、例外が呼び出し元まで伝播しているということですね。
$ErrorActionPreference はスクリプト ファイルごとに保持されたりはしません。
外部アプリを呼び出す場合
先ほどのスクリプトを、こう書き換えると、挙動が変わります。
# caller.ps1 $ErrorActionPreference = 'Continue' Write-Host 'Hello' pwsh.exe -File .\callee.ps1 Write-Host 'World'
この場合は、callee.ps1 で ErrorAction に Stop を指定していても、コンソールには「World」まで出力されます。
エラーで中断されるのは、あくまで子プロセスとして呼び出した pwsh.exe だけであって、例外は親プロセスまでは伝播しません。
こういう場合にはどうやってエラーを判断するかというと、$LASTEXITCODE という変数を使います。
これには、実行した外部アプリの終了コードが入ってきます。
# caller.ps1 pwsh.exe -Command 'Write-Host "Hello"' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" pwsh.exe -File .\callee.ps1 Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
こうすると、2 回目の $LASTEXITCODE の出力では 1 が表示されます。
なお、先に「Hello」を表示するのに pwsh.exe を実行しているのは、確実に成功するコマンドを実行することで、$LASTEXITCODE を 0 にリセットするためです。
PowerShell スクリプト内で、別の PowerShell スクリプトを呼び出すのに、わざわざ pwsh.exe を実行することはそんなにないとは思いますが、他の exe ファイルなどを実行する場合でも同じことが言えます。
外部アプリに終了コードを返す場合
exit 文を使うことで、PowerShell スクリプトから呼び出し元に、任意の終了コードを返すことができます。
以下のように書けば、終了コードは 5 になります。
exit 5
ただし、ErrorAction パラメーターをはじめとして、「中断されるエラー」で終了した場合の終了コードは 1 になります。
従って、例えば、以下のように書いても、呼び出し元に伝えられる終了コードは 5 ではなく 1 になります(exit 5 の行は実行されません)。
# caller.ps1 pwsh.exe -Command 'Write-Host "Hello"' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" pwsh.exe -File .\callee.ps1 Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
# callee.ps1 Write-Error 'Oops!!' -ErrorAction Stop exit 5
中断されるエラーが発生せず、かつ、exit 文で終了コードを明示しなかった場合、中断されないエラーが発生していても、終了コードは 0 になります。
スクリプト ファイルを呼び出した場合の終了コード
以下のように書くと、終了コードは 5 になります。
callee.ps1 で ErrorAction を指定していないことに注意してください。
# caller.ps1 $ErrorActionPreference = 'Continue' pwsh.exe -Command 'Write-Host "Hello"' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" & .\callee.ps1 Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
# callee.ps1 Write-Error 'Oops!!' exit 5
では、以下のようなスクリプトだとどうなるでしょうか。
# caller.ps1 $ErrorActionPreference = 'Continue' pwsh.exe -Command 'Write-Host "Hello"' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" try { & .\callee.ps1 } catch { } Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
# callee.ps1 Write-Error 'Oops!!' -ErrorAction Stop exit 5
この場合、$LASTEXITCODE は 0 です。これは事前にリセットしたものが残っているためです。
スクリプトの直接実行のため例外は呼び出し元の caller.ps1 まで伝播します。この場合の終了コードは 1 にはなりません。
以下のように、事前に $LASTEXITCODE を 0 にリセットせず、外部コマンドも呼び出さない場合、$LASTEXITCODE は未設定です。
# caller.ps1 $ErrorActionPreference = 'Continue' Write-Host 'Hello' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" try { & .\callee.ps1 } catch { } Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
# callee.ps1 Write-Error 'Oops!!' -ErrorAction Stop exit 5
Windows PowerShell の怪しい挙動
以下のようなスクリプトを実行してみましょう。
callee.ps1 を呼び出す際に、pwsh.exe ではなく powershell.exe を使っています。
# caller.ps1 $ErrorActionPreference = 'Continue' pwsh.exe -Command 'Write-Host "Hello"' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" powershell.exe -File .\callee.ps1 Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
# callee.ps1 Write-Error 'Oops!!' exit 5
この場合、$LASTEXITCODE は 5 になります。いいですね。
では、こうすると?
# caller.ps1 $ErrorActionPreference = 'Continue' pwsh.exe -Command 'Write-Host "Hello"' Write-Host "`$LASTEXITCODE = $LASTEXITCODE" powershell.exe .\callee.ps1 Write-Host "`$LASTEXITCODE = $LASTEXITCODE"
違いは、callee.ps1 を呼び出す際の -File パラメーターの有無だけです。
なんと、こう書くと、$LASTEXITCODE は 1 になります。なんで。
なお、PowerShell Core (pwsh.exe) の場合、(少なくとも v6.2.3 では)-File の有無にかかわらず 5 になります。
組み込み変数 $?
個人的に使ったことはないのですが、直前の実行の成否を示す特殊変数 $? があります。
この値は、直前に実行したコマンドが成功した場合は $true、失敗した場合は $false に設定されます。エラーが中断されるエラーか、中断されないエラーかに関わらず、失敗すれば $false になります。
Write-Host でも成功すれば $true になってしまうので、挙動を観察するときは要注意です。
また、外部コマンドを実行した直後の場合、そのコマンドの終了コードが 0 であれば $true、0 以外の場合は $false になるそうです。
PowerShell 7 の新機能
PowerShell 7 からは、Pipeline Chain Operator というのが搭載されるそうです。
他のスクリプト言語にある、「前段のコマンドが成功した場合のみ後段のコマンドを実行する」とか「前段のコマンドが失敗した場合のみ後段のコマンドを実行する」とかいう機能です。
こちらについて、詳しくはこちらのブログをご覧ください。
dev.classmethod.jp
おわりに
いろいろなケースを取り上げましたが、自分でも試していて意外な挙動をした点がありました。反省。
スクリプトを書く際には注意してください。
なお、PowerShell Advent Calendar 2019 は、寄稿していただける方を絶賛募集中です。よろしくお願いします。
qiita.com