PowerShell のエラーハンドリングを(今度こそ)理解する
PowerShell はエラー処理がとても難しい言語として知られています。
きちんと理解しようとすると途端に深みにハマってしまいます。
それこれもエラーの挙動が正しくドキュメント化されていないことが原因なのですが。
そこで、今度こそ PowerShell のエラーをきちんと理解しようといろいろ調べました。
結果としてまだまだ腑に落ちない箇所がありつつも、一定程度は理解できたような気がしましたのでここにまとめておきたいと思います。
おことわり
-
本記事は PowerShell の中の人である Michael Klement 氏がこちらのGitHub Issueにコメントしている内容を基に、筆者の独自検証の結果やそれに基づく解釈を加えています
-
PowerShell の実際の挙動を把握することに重点を置いているため、公式ドキュメントに記載のない内容や独自の用語を記載・使用しています
-
内容の正確性は一切保証しません。間違いがありましたらやんわりとご指摘いただければ幸いです
検証環境
-
以下バージョンのPowerShellで挙動を確認しています
特に PowerShell 7 はまだプレビューなので今後動作が変わる可能性があります- Windows PowerShell 5.1
- PowerShell Core 6.2.3
- PowerShell 7.0.0-preview.5
-
Windows 10 バージョン1903のみで挙動を確認しています
エラーの種類
PowerShellのエラーには
- 終了しないエラー (Non-terminating Errors)
- ステートメント終了エラー (Statement-terminating Errors)
- スクリプト終了エラー (Script-terminating Errors)
の3種類のエラーがあります。
PowerShell のドキュメントには 終了しないエラー(Non-terminating Errors) と 終了するエラー(Terminating Errors) の2種類しか記載がありませんが、その2種類では PowerShell の実際の挙動を説明するのに十分ではないため、ここでは 終了するエラー をさらに スクリプト終了エラー と ステートメント終了エラー に細分化して説明していきます
より厳密に"エラー"を分類するならここに挙げる3種類のエラーの他にもう一つ 構文解析エラー (Parsing Errors) を追加すべきかもしれません。これはPowerShellコマンドの構文に致命的な問題がある場合に発生するエラーで、PowerShellスクリプトが実行される前に発生します。このエラーをスクリプト内でハンドリングする方法は存在しません。そのためこの記事内では扱わないことにしました。
終了しないエラー (Non-terminating Errors)
-
このエラーが発生した場合でも、スクリプトは停止せずその後の処理は続行されます
-
発生時にはコンソールにエラーメッセージが出力されるとともに、エラー情報が
$Error
自動変数に格納されます
これはスクリプト終了エラーおよびステートメント終了エラーの場合も同様です -
PowerShellのコマンドレットが出力するエラーのほとんどが終了しないエラーです
Get-Item -Path "存在しないパス" #ここでエラーが発生するが、次の行も実行される
Write-Output "処理終了"
ステートメント終了エラー (Statement-terminating Errors)
-
PowerShellのドキュメント上では 終了するエラー (Terminating Errors) として扱われます
-
このエラーが発生した場合、実行中のステートメントが終了しますが呼び出し元のスクリプトは継続実行されます
-
PowerShellのランタイムエラー や .NETの例外 (Exceptions) はステートメント終了エラーです
- ランタイムエラーの例:
1 / 0
- .NET例外の例:
[int]::Parse("hoge")
- ランタイムエラーの例:
[int]::Parse("hoge") #.NET例外発生
Write-Output "処理終了" #この行は実行される
スクリプト終了エラー (Script-terminating Errors)
-
PowerShellのドキュメント上では 終了するエラー (Terminating Errors) として扱われます
-
このエラーが発生した時点で、その呼び出し元も含め、現在実行中のスクリプト全体が即座に終了します
-
スクリプト終了エラーを発生させる唯一の方法は
throw
キーワードを使うことです
throw
Write-Output "処理終了" #この行は実行されない
サンプルスクリプト
3つのエラーの違いが分かるよう、もう少し詳しい例を示します。
- 終了しないエラー
function Invoke-NonTerminatingError {
[CmdletBinding()] param()
Write-Output "関数開始"
Get-Item -Path "存在しないパス" # 終了しないエラー発生
Write-Output "関数終了"
}
Write-Output "スクリプト開始"
Invoke-NonTerminatingError
Write-Output "スクリプト終了"
PS C:> .\test.ps1
スクリプト開始
関数開始
ObjectNotFound: (C:\test\存在しないパス:String) [Get-Item], ItemNotFoundException
関数終了
スクリプト終了
関数の実行中にエラーが発生しましたが、関数内の後続処理も呼び出し元の処理も継続していることが分かります。
- ステートメント終了エラー
function Invoke-StatementTerminatingError {
[CmdletBinding()] param()
Write-Output "関数開始"
# ステートメント終了エラー発生
$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
'exception message',
'errorId',
[System.Management.Automation.ErrorCategory]::InvalidResult,
$null
)
)
Write-Output "関数終了"
}
Write-Output "スクリプト開始"
Invoke-StatementTerminatingError
Write-Output "スクリプト終了"
PS C:> .\test.ps1
スクリプト開始
関数開始
InvalidResult: (:) [Invoke-StatementTerminatingError], Exception
スクリプト終了
ステートメント終了エラーを意図的に発生させるために$PSCmdlet.ThrowTerminatingError()
というメソッドを使用しています(詳細は後述)
ステートメント終了エラーが発生した時点で関数Invoke-StatementTerminatingError
が終了したため、Write-Output "関数終了"
は実行されていません。しかし、呼び出し元の処理は継続されるためWrite-Output "スクリプト終了"
は実行されています。
- スクリプト終了エラー
function Invoke-ScriptTerminatingError {
[CmdletBinding()] param()
Write-Output "関数開始"
throw # スクリプト終了エラー発生
Write-Output "関数終了"
}
Write-Output "スクリプト開始"
Invoke-ScriptTerminatingError
Write-Output "スクリプト終了"
PS C:> .\test.ps1
スクリプト開始
関数開始
OperationStopped: (:) [], Exception
throw
を実行した時点ですべての処理が終了し、関数も呼び出し元も後続の処理は実行されていません。
エラー発生をトラップする方法
try
ステートメント
-
ステートメント終了エラーおよびスクリプト終了エラーは
try
ステートメントを使用してトラップできます。 -
終了しないエラーは
try
でトラップできません。
後述する$ErrorActionPreference
変数や-ErrorAction
共通パラメータを使用したエラー挙動変更と併用することで、try
でトラップが可能になります。
try {
#ステートメント終了エラーもしくはスクリプト終了エラーが発生する可能性がある処理
Write-Output 'Start'
throw 'Error!' #スクリプト終了エラー
Write-Output 'End' #実行されない
}
catch {
#エラー発生時の処理
Write-Output 'Catch!'
Write-Output ('Error message is ' + $_.Exception.Message)
}
-
try
ブロック内でエラーが発生した時点でcatch
ブロックに処理が移行します -
try
ブロック内のエラー発生後の後続処理は実行されません。 -
コンソールへのエラーメッセージ出力もされませんが、
$Error
自動変数にはエラー情報が格納されます -
発生したエラーを含むエラーレコードは
catch
ブロック内に限り$_
自動変数に格納されています -
catch
ブロック内でthrow
ステートメントを引数なしで実行すると、try
ブロック内で発生したエラーを再スローできます
この場合、発生したエラーの種類に関わらず必ずスクリプト終了エラーとして再スローされます
catch
対象の例外型を指定したり、エラー発生有無に関わらず必ず実行されるfinally
ブロックを使用する例も載せておきます。
try {
Non-ExsitenceCommand
}
catch [System.Management.Automation.CommandNotFoundException] {
Write-Output "そんなコマンドは存在しない!"
}
finally {
Write-Output "終了"
}
エラーをトラップする必要はないけど、エラー発生後にも実行したい処理がある場合、catch
ブロックのないtry-finally
という書き方も利用可能です。
try {
# エラーが発生する可能性がある処理...
}
finally {
Write-Output "これは必ず実行される"
}
trap
ステートメント
trap
ステートメントはtry-catch
に比べて扱いづらいので利用する機会は少ないかと思いますが、挙動を正しく理解して使うと便利なことが(ごく稀に)あります。
-
ステートメント終了エラーおよびスクリプト終了エラーは
trap
ステートメントを使用してトラップできます。 -
終了しないエラーは
trap
でトラップできません。
後述する$ErrorActionPreference
変数や-ErrorAction
共通パラメータを使用したエラー挙動変更と併用することで、trap
でトラップが可能になります。
trap {
# エラー発生時の処理
Write-Output 'Error Occurred.'
}
Write-Output 'Start'
throw #スクリプト終了エラー
Write-Output 'End'
Start
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:7 文字:1
+ throw #スクリプト終了エラー
+ ~~~~~
+ CategoryInfo : OperationStopped: (:) [], RuntimeException
+ FullyQualifiedErrorId : ScriptHalted
End
-
trap
ステートメントと同じか内包されたスコープでエラーが発生した際に、trap
ブロック内の処理が実行されます -
trap
は同じスコープ内であればどこに書いても構いません
スクリプトの最後などエラー発生行より後に書いても正しくトラップされます -
try-catch
とは異なり、コンソールにはエラーメッセージが出力され、trap
ブロックと同じスコープにあるエラー発生箇所以後の処理から継続実行されます
trap
ステートメントとスコープの関係性
trap
ステートメントはスクリプト内のどこに書くか(どのスコープに属するか)を意識していないと予期せぬ挙動になることがあります。
function function1 {
trap { Write-Output 'Error Occurred.' }
throw #スクリプト終了エラー
Write-Output "function1 終了" #実行される
}
function1
Write-Output "スクリプト終了"
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:3 文字:5
+ throw #スクリプト終了エラー
+ ~~~~~
+ CategoryInfo : OperationStopped: (:) [], RuntimeException
+ FullyQualifiedErrorId : ScriptHalted
function1 終了
スクリプト終了
上記例の場合、trap
はfunction1
内のスコープに属しているためthrow
でスクリプト終了エラーが発生した後、trap
内の処理が実行され、処理はthrow
の次の行に戻ります。よってWrite-Output "function1 終了"
は実行されます。
function function2 {
throw #スクリプト終了エラー
Write-Output "function2 終了" #実行されない
}
trap { Write-Output 'Error Occurred.' }
function2
Write-Output "スクリプト終了"
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:2 文字:5
+ throw #スクリプト終了エラー
+ ~~~~~
+ CategoryInfo : OperationStopped: (:) [], RuntimeException
+ FullyQualifiedErrorId : ScriptHalted
スクリプト終了
上記例では、trap
はスクリプト全体のスコープに属しています。
function2
のスコープはスクリプト全体スコープに内包されているため、function2
内のエラーもトラップされてtrap
内の処理が実行されますが、処理はtrap
と同じスクリプト全体スコープの次の行であるWrite-Output "スクリプト終了"
に戻ります。
よってWrite-Output "function2 終了"
は実行されません。
continue
、break
キーワード
trap
ブロック内ではcontinue
とbreak
キーワードを使用してエラートラップ後の挙動を変更できます。
trap {
Write-Output 'Error Occurred.'
continue
}
Write-Output 'Start'
throw #スクリプト終了エラー
Write-Output 'End' #trap後に実行される
Start
Error Occurred.
End
trap
ブロック内でcontinue
が現れると、trap
内の処理はそこで終了しtrap
後の処理が続行されます。
また、コンソールにエラーメッセージが出力されなくなります。
trap {
Write-Output 'Error Occurred.'
break
}
Write-Output 'Start'
throw #スクリプト終了エラー
Write-Output 'End' #実行されない
Start
Error Occurred.
ScriptHalted
発生場所 F:\ErrorHandlingPS\test.ps1:7 文字:3
+ throw #スクリプト終了エラー
+ ~~~~~
+ CategoryInfo : OperationStopped: (:) [], RuntimeException
+ FullyQualifiedErrorId : ScriptHalted
trap
ブロック内でbreak
が現れると、エラーの種類や後続処理の有無に関わらずそこでスクリプトが終了します。
trap
が関数内のスコープに属している場合でも、呼び出し元も含めてスクリプト全体が終了することに注意してください。
エラー発生時の挙動を変更する方法
$ErrorActionPreference
自動変数
$ErrorActionPreference
自動変数の 値を変更することでスクリプト単位でエラー発生時の挙動を変更することができます。
PowerShellではデフォルトでContinue
が指定されています。
-
Continue
- デフォルトの挙動 -
Stop
- すべてのエラーの挙動が スクリプト終了エラー に変更されます -
SilentlyContinue
-
終了しないエラー および ステートメント終了エラー の場合
エラーとしての挙動は変化しませんが、コンソールにエラーメッセージが出力されなくなります -
スクリプト終了エラー の場合
終了しないエラーとほぼ同じ挙動になり、エラー発生時にも処理が継続するようになります
ただし、try-catch
やtrap
でのエラートラップは引き続き可能です
コンソールにエラーメッセージは出力されません
-
-
Inquire
- エラー発生時に対話型のダイアログでユーザに動作を選択させます
使い方
$ErrorActionPreference = "Stop"
Write-Error "Error!" #処理はここで終了します
Write-Output "End"
-ErrorAction
共通パラメータ
PowerShellのコマンドレットおよび高度な関数には-ErrorAction
というパラメータが備わっています。このパラメータを使用するとコマンド単位でエラー発生時の挙動を変更できます。
-ErrorAction
パラメータの指定値は$ErrorActionPreference
自動変数での設定値よりも優先されます。
-
Continue
- デフォルトの挙動 -
Stop
-
終了しないエラーの場合
挙動がスクリプト終了エラーと同等に変更されます -
ステートメント終了エラーの場合
ほぼ挙動は変化しませんが、例外的に関数呼び出し元で$?
変数にセットされる値が変わるようです。(これが動作仕様なのかバグなのかは分かりません)# ステートメント終了エラーが発生する関数 function func1 {[CmdletBinding()]Param() 1 / 0 } func1 # -ErrorActionを指定せず呼び出し $? #Trueになる func1 -ErrorAction Stop $? #Falseに変わる
-
スクリプト終了エラー の場合 - 挙動は変化しません
-
-
SilentlyContinue
-
終了しないエラーの場合
エラーとしての挙動は変化しませんが、コンソールにエラーメッセージが出力されなくなります -
ステートメント終了エラーの場合
挙動は変化しません コンソールにもエラーメッセージが表示されます -
スクリプト終了エラー の場合
終了しないエラーにSilentlyContinueを指定した場合とほぼ同じ挙動になります
エラー発生時にも処理が継続します
コンソールにエラーメッセージは出力されません
ただし、try-catch
やtrap
でのエラートラップは引き続き可能です
-
-
Ignore
-
終了しないエラーの場合
コンソールにエラーメッセージが出力されなくなり、$Error
自動変数にエラー情報が格納されなくなります
エラー発生しなかったことになるわけではありません -
ステートメント終了エラーの場合
挙動は変化しません コンソールにもエラーメッセージが表示されますし、$Error
自動変数にもエラー情報が格納されます -
スクリプト終了エラー の場合
終了しないエラーにSilentlyContinueを指定した場合とほぼ同じ挙動になります
コンソールにエラーメッセージは出力されませんが、$Error
自動変数にはエラー情報が格納されます
try-catch
やtrap
でのエラートラップは引き続き可能です
補足:
-ErrorAction
にIgnore
を指定できないバグについて
v6 以前の PowerShell には高度な関数に対して-ErrorAction Ignore
を指定するとNotSupportedException
(ステートメント終了エラー)が発生するバグがあります。このバグは PowerShell 7 で解消される予定です。 -
使い方
$ErrorActionPreference = "Stop"
# -ErrorActionPreference は $ErrorAction より優先されるため、
# 以下のコマンドでエラーが発生してもスクリプトは継続実行される
Get-Item -Path '存在しないパス' -ErrorAction 'Continue'
Write-Output "End"
エラーロギング
$Error
自動変数
-
いずれのエラーも、エラー発生時に
$Error
という自動変数にエラー情報が格納されます。 -
$Error
はArrayList
型の配列になっていて、一番最後に発生したエラーが配列の先頭に格納されるスタック型です。
よって最も直近に発生したエラー情報には$Error[0]
でアクセスできます。 -
基本的にエラー発生時は必ず
$Error
自動変数にエラー情報が記録されます。try-catch
によるエラートラップや、$ErrorActionPreference = 'SilentlyContinue'
によるメッセージ出力抑制をした場合でも記録はされます。 -
唯一の例外は終了しないエラーに
-ErrorAction Ignore
を組み合わせた場合で、この場合は$Error
に情報は記録されません。 -
エラー情報は
[System.Management.Automation.ErrorRecord]
型オブジェクトで記録され、様々な情報が格納されています。
Write-Error 'Error!!!'
$Error[0] | Select-Object *
writeErrorStream : True
PSMessageDetails :
Exception : Microsoft.PowerShell.Commands.WriteErrorException: Error!!!
TargetObject :
CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,test.ps1
ErrorDetails :
InvocationInfo : System.Management.Automation.InvocationInfo
ScriptStackTrace : <ScriptBlock>、F:\ErrorHandlingPS\test.ps1: 行 1
PipelineIterationInfo : {0, 0, 0}
よく使うであろう処理をいくつか紹介します
# 発生した例外オブジェクトを取得
$Error[0].Exception
# 発生した例外の型名を取得
$Error[0].Exception.GetType().Fullname
# エラーメッセージを文字列で取得
$Error[0].Exception.Message
# エラースタックをクリア
$Error.Clear()
Get-Error
コマンドレット
-
PowerShell 7 Preview.5 から
Get-Error
というコマンドレットが実装されました -
このコマンドを引数なしで実行すると直近に発生したエラー情報1件が取得・表示できます
内部処理的にはほぼ$Error[0]
を呼び出しているだけです。 -
以下のように
-Newest
パラメータで指定件数のエラー情報を取得することもできます
# 直近10件のエラーを取得
Get-Error -Newest 10
# 以下のコマンドと等価です
$Error[0..10]
-
$Error
自動変数を使えばいいので、下位互換性のことを考えるとあまり利用するシーンは無い気がします
$?
自動変数
$?
自動変数には、直前に実行した処理が成功したか否かが[bool]
型で記録されます。
-
True
- 直前の処理でエラーが発生しなかったことを示します -
False
- 直前の処理で何らかのエラーが発生したことを示します
ただし、$?
の指す「直前に実行した処理」というのが、一体なにを指すのか明確に定義されていないため、直感的に予想する結果と異なる結果になることが少なくありません。
$?
がFalse
になる例
Write-Error 'Error' #終了しないエラー
$? #False
[int]::Parse('hoge') #ステートメント終了エラー
$? #False
try {
throw #スクリプト終了エラーをtry-catch (catchブロックが空)
}
catch { }
$? #False
ここまでは直感的に理解できる結果だと思います
$?
が直感と異なる結果になる例
# 1.終了しないエラー + "-ErrorAction Ignore" 指定
Write-Error 'Error' -ErrorAction Ignore
$? #False
# 2.終了しないエラーを(...)でラップ
(Write-Error 'Error')
$? #FalseになりそうだけどTrue
# 3.ステートメント終了エラーを(...)でラップ
([int]::Parse('hoge'))
$? #終了しないエラーはTrueだったのにステートメント終了エラーはFalse
# 4.条件分岐の判定式内での終了しないエラー
if (Write-Error 'Error') { }
$? #True
# 5. 関数内での終了しないエラー
function function1 { Write-Error 'Error' }
function1
$? #True
# 6. 関数内でのステートメント終了エラー
function function2 { [int]::Parse('hoge') }
function2
$? #True
あえてかなり嫌らしい挙動を示す例を挙げています。
-
1.は
-ErrorAction
パラメータにIgnore
を指定してもエラーが無かったことになるわけではないことを示します。
メッセージも表示されないし$Error
に記録もされませんが、エラーは確かに発生したのです。 -
2.の挙動は特に理解し難いです。
PowerShellでは他言語と異なり()
自体が式として定義されています。()
式はその中の処理を実行した後、その実行結果を出力する動作をします。すなわち$?
の直前に実行された処理は()
内の処理ではなく、()
式が実行結果を出力する処理であるため、$?
はTrue
を返すとする解釈になります。 -
3.については
(...)
までが一つのステートメントとして扱われるため、内部でステートメント終了エラーが発生することで外側の(...)
まで含めエラー終了と判定されると解釈します。 -
4.は条件分岐の判定式内の終了しないエラーは
$?
自動変数に反映しないという特例です。
なお、例示はしていませんが、判定式内のステートメント終了エラーは$?
に反映されます。 -
5.および6.の挙動は私は理解できていません、現状そうなるということで紹介しておきます
自作関数の場合は$PSCmdlet.WriteError()
や$PSCmdlet.ThrowTerminatingError()
を使うことでこの謎挙動を回避することができます(詳細は後述)
余談:
PowerShellには$?
の他に$LASTEXITCODE
という自動変数がありますが、
これは外部コマンドの終了コードが格納される変数であり、$?
とは明確に異なるものです。
エラーを発生させる方法
終了しないエラー
Write-Error
コマンドレット
- 終了しないエラーを発生させる最も一般的な方法は
Write-Error
コマンドレットを使う方法です
# 1.エラーメッセージを文字列で与える方法
# この場合の例外型は [Microsoft.PowerShell.Commands.WriteErrorException] になる
Write-Error -Message "エラーメッセージ" # '-Message' は省略可能
# 2.任意の[Exception]を継承したオブジェクトを与える方法
$ex = [System.Exception]::new()
Write-Error -Exception $ex
# 3. 任意の[System.Management.Automation.ErrorRecord]オブジェクトを与える方法
$er = [System.Management.Automation.ErrorRecord]::new(
'exception message',
'errorId',
[System.Management.Automation.ErrorCategory]::InvalidResult,
$null
)
Write-Error -ErrorRecord $er
- ただし、
Write-Error
は関数内などで使用した場合に、その呼び出し元スコープで$?
自動変数が正しく設定されないという問題があります。これはWrite-Error
の代わりに$PSCmdlet.WriteError()
メソッドを使うことで回避できますが、高度な関数でしか使えない方法になります。
function function1 {
[CmdletBinding()]
param()
$er = [System.Management.Automation.ErrorRecord]::new(
'exception message',
'errorId',
[System.Management.Automation.ErrorCategory]::InvalidResult,
$null
)
$PSCmdlet.WriteError($er) #Write-Errorの代わりに使う
}
function1
$? #False (Write-Errorを使った場合はTrueになる)
ステートメント終了エラー
$PSCmdlet.ThrowTerminatingError
メソッド
-
任意のステートメント終了エラーを明示的に発生させる唯一の方法と考えられています
-
高度な関数内でのみ使用可能です
function function2 {
[CmdletBinding()]
param()
$er = [System.Management.Automation.ErrorRecord]::new(
'exception message',
'errorId',
[System.Management.Automation.ErrorCategory]::InvalidResult,
$null
)
$PSCmdlet.ThrowTerminatingError($er)
}
function2
スクリプト終了エラー
throw
ステートメント
- スクリプト終了エラーを発生させる最も一般的で簡潔な方法です
# 引数なしで呼び出した場合、エラーメッセージは"ScriptHalted"
# 例外型は[System.Management.Automation.RuntimeException]
throw
# 任意のオブジェクトを渡した場合、エラーレコードのTargetObjectに格納される
throw 1234
$Error[0].TargetObject #1234
# [Exception]型を継承したオブジェクトを渡した場合、その例外がthrowされる
throw [System.Management.Automation.ItemNotFoundException]::new()
$Error[0].Exception.GetType().Name #ItemNotFoundException
Pipeline Chain Operators
PowerShell 7 Preview.5 から実装された Pipeline Chain Operators を紹介しておきます。Pipeline Chain Operators のエラー処理は PowerShell-RFC にも詳しく記載されているので併せて読んでいただくのが良いかと思います。
なお、Pipeline Chain Operators は現在 Experimental の扱いなので、以下コマンドで有効化しないと使えないかもしれません。(Preview 版の PowerShell は既定で有効になっています)
Enable-ExperimentalFeature -Name PSPipelineChainOperators
&&
演算子
-
&&
で2つ以上のパイプラインをつなげると、前のパイプラインが成功した場合のみ、次のパイプラインが実行されます
# すべてのパイプラインが成功する
Write-Output 'AAA' && Write-Output 'BBB' && Write-Output 'CCC'
AAA
BBB
CCC
# 2つめのパイプラインで終了しないエラーが発生するので3つめのパイプラインは実行されない
Write-Output 'AAA' && Write-Error 'ERROR' && Write-Output 'CCC'
AAA
Write-Output 'AAA' && Write-Error 'ERROR' && Write-Output 'CCC' : ERROR
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException
||
演算子
-
||
で2つ以上のパイプラインをつなげると、前のパイプラインが失敗した場合のみ、次のパイプラインが実行されます
# 最初のパイプラインが成功するので2つめのパイプラインは実行されない
Write-Output 'AAA' || Write-Output 'BBB'
AAA
# 最初のパイプラインで終了しないエラーが発生するので2つめのパイプラインが実行される
Write-Error 'ERROR' || Write-Output 'BBB'
Write-Error 'ERROR' || Write-Output 'BBB' : ERROR
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException
BBB
Pipeline Chain Operators におけるエラーハンドリング
-
&&
演算子も||
演算子も、パイプラインが成功したか失敗したかの判定に$?
自動変数を使っています
つまり各演算子は以下のコードと等価です
# Write-Output 'AAA' && Write-Output 'BBB' と同等のコード
Write-Output 'AAA'
if ($?) { Write-Output 'BBB' }
# Write-Error 'ERROR' || Write-Output 'BBB' と同等のコード
Write-Error 'ERROR'
if (-not $?) { Write-Output 'BBB' }
- 前述のように
$?
演算子は直感的でない結果を返す場合があるため、 Pipeline Chain Operators を使ったコードも直感と異なる挙動を示すことがあります
# 2つめのパイプラインは実行されない
(Write-Error 'ERROR') || Write-Output 'BBB'
#ステートメント終了エラーが発生する関数
function f1 { [CmdletBinding()]param() 1 / 0 }
# 2つめのパイプラインは実行されない
f1 || Write-Output 'BBB'
# 2つめのパイプラインは実行される
f1 -ErrorAction Stop || Write-Output 'BBB'
- スクリプト終了エラーが発生した場合、Pipeline Chain Operators は実行されません
# 2つめのパイプラインは実行されない
throw || Write-Output 'BBB'
-
try-catch
やtrap
によりエラートラップされた場合、Pipeline Chain Operators は実行されることなくcatch
やtrap
ブロックに処理が移行します
try {
# 2つめのパイプラインは実行されない
[int]::Parse('hoge') || Write-Output 'BBB'
}
catch {
Write-Output 'Catch'
}
- bashに習熟しているとエラーハンドリングとして
||
とフロー制御式(return
やexit
)を組み合わせて使いたくなるかもしれませんが、PowerShellにおいてフロー制御式はパイプラインではないため、Pipeline Chain Operators の左右に置くことはできません
# Get-Itemコマンドが失敗した場合にexitで処理を打ち切りたいとする
# これはエラーになる
Get-Item -Path 'hoge' || exit 1
# 部分式演算子$()を使えば回避することは可能
Get-Item -Path 'hoge' || $( exit 1 )
# PowerShell v6 以前でも使える等価な書き方
Get-Item -Path 'hoge'
if (-not $?) {
exit 1
}
ベストプラクティス
PowerShell 7 で Pipeline Chain Operators が登場したことで自作関数を書く際のカスタムエラーの出力方法をどうすればいいのか、かなりややこしくなってきました....
挙動が変わる可能性もあるので、あくまで暫定でこうすると良さそうだ、というレベルで書いておきます。
関数を書くときは必ず高度な関数にする
-
その場で書き捨てするような場合を除き、PowerShell で関数を書くのであれば必ず高度な関数で書きましょう
-
高度な関数というのは冒頭に
[CmdletBinding()]
が付いているやつです -
高度な関数にしないと
-ErrorAction
共通パラメータが使えません
# 普通の関数
function Normal-Function {
Write-Output 'Hello'
}
# 高度な関数
function Advanced-Function {
[CmdletBinding()]
param()
Write-Output 'Hello'
}
自作関数では終了しないエラーを出す
-
PowerShell から呼び出すための関数であれば、その関数が出すエラーは基本的にすべて 終了しないエラー としてしまうのが良いかと思います。呼び出し元からすると最もエラーハンドリングの融通が効くので。
-
エラー発生時に処理を打ち切りたければ
Write-Error
実行直後にreturn
で終了してしまえばいいです -
関数内で別のコマンドや.NETメソッドを呼び出す際はなるべく
try-catch
を使い、終了しないエラー以外のエラーが発生しないように気をつけます -
関数が Pipeline Chain Operators と組み合わせて呼び出される可能性がある場合は、
Write-Error
は使わず$PSCmdlet.WriteError()
を使いましょう
$?
および Pipeline Chain Operators は使わない
-
$?
を信用してはいけません -
ただし自作関数が自分以外の誰かによって呼ばれる可能性がある場合、これらを使われないとは限りません
外部公開するモジュールを作成している場合などは考慮しておく必要があるでしょう
throw
はなるべく使わない
-
特に関数内では
throw
を不用意に使わないように。呼び出し元まで巻き込みます -
関数内でどうしても終了するエラーを出したい場合は
$PSCmdlet.ThrowTerminatingError()
でステートメント終了エラーを使いましょう -
(そのような状況があるのか分かりませんが)関数内でやむを得ず
throw
を使う場合、必ずthrow
実行直後にreturn
などで明示的に処理をを終了します
これは-ErrorAction SilentlyContinue
を指定された場合にthrow
以後の処理が意図せず実行されることを防ぐためです
(throw
の代わりに$PSCmdlet.ThrowTerminatingError()
を使えばこの状況も回避可能です)
function Invoke-Throw {
[CmdletBinding()]
Param()
#なにか致命的なエラーが発生してthrowで処理を打ち切りたくなったとする
throw '致命的なエラー!'
# throwで処理が終わるだろうと思い込んで後続に処理を書いてしまうと...
Write-Output '後続処理'
}
# 関数がこのように呼び出だされた時、関数内の'後続処理'は実行されてしまう
Invoke-Throw -ErrorAction SilentlyContinue