86
103

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PowerShell のエラーハンドリングを(今度こそ)理解する

Last updated at Posted at 2019-11-03

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 終了
スクリプト終了

上記例の場合、trapfunction1内のスコープに属しているため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 終了"実行されません

continuebreakキーワード

trapブロック内ではcontinuebreakキーワードを使用してエラートラップ後の挙動を変更できます。

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-catchtrapでのエラートラップは引き続き可能です
      コンソールにエラーメッセージは出力されません

  • 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-catchtrapでのエラートラップは引き続き可能です

  • Ignore

    • 終了しないエラーの場合
      コンソールにエラーメッセージが出力されなくなり、$Error自動変数にエラー情報が格納されなくなります
      エラー発生しなかったことになるわけではありません

    • ステートメント終了エラーの場合
      挙動は変化しません コンソールにもエラーメッセージが表示されますし、$Error自動変数にもエラー情報が格納されます

    • スクリプト終了エラー の場合
      終了しないエラーにSilentlyContinueを指定した場合とほぼ同じ挙動になります
      コンソールにエラーメッセージは出力されませんが、$Error自動変数にはエラー情報が格納されます
      try-catchtrapでのエラートラップは引き続き可能です

    補足:-ErrorActionIgnoreを指定できないバグについて
    v6 以前の PowerShell には高度な関数に対して-ErrorAction Ignoreを指定するとNotSupportedException(ステートメント終了エラー)が発生するバグがあります。このバグは PowerShell 7 で解消される予定です。

使い方

$ErrorActionPreference = "Stop"
# -ErrorActionPreference は $ErrorAction より優先されるため、
# 以下のコマンドでエラーが発生してもスクリプトは継続実行される
Get-Item -Path '存在しないパス' -ErrorAction 'Continue'
Write-Output "End"

エラーロギング

$Error自動変数

  • いずれのエラーも、エラー発生時に$Errorという自動変数にエラー情報が格納されます。

  • $ErrorArrayList型の配列になっていて、一番最後に発生したエラーが配列の先頭に格納されるスタック型です。
    よって最も直近に発生したエラー情報には$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-catchtrapによりエラートラップされた場合、Pipeline Chain Operators は実行されることなくcatchtrapブロックに処理が移行します
try {
  # 2つめのパイプラインは実行されない
  [int]::Parse('hoge') || Write-Output 'BBB'
}
catch {
  Write-Output 'Catch'
}
  • bashに習熟しているとエラーハンドリングとして ||とフロー制御式(returnexit)を組み合わせて使いたくなるかもしれませんが、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
86
103
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
103

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?