PowerShell エラーハンドリングの方法について その2 Trap編

PowerShell エラーハンドリングの方法について その2 Trap編

こんにちは!やましー@データ活用クラウドエンジニア(@yamashi18041)です!

今日は前回に引き続き続き PowerShellのエラーハンドリング第2弾です。

前回はPowerShellのエラーハンドリングの基本中の基本である、Try-Catchについて検証してみました。

今回はTrapにより例外をハンドリングしてみたいと思います。

検証した環境はこんな感じ

PS C:\> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.18362.145
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.18362.145
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

PowerShellのエラーハンドリングの種類

PowerShellのエラーハンドリングの方法としては

エラーハンドリングする方法にはいくつかパターンがります。

今回はTrapについて書いてみたいと思います。

Trapによる例外ハンドリング

今回は公式ドキュメントを参考にしてTrapについて解説します

https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_trap?view=powershell-5.1

Trapの仕様

Trapの仕様は

  1. トラップできるのは終了するエラーです。終了しないエラーはトラップできません。
  2. TrapはTrapステートメントを書いたスコープで発生した終了するエラーをトラップします。
  3. Trapステートメントはスコープ内どこに記述してもOK。そのスコープ内で発生した終了するエラーをトラップします。
  4. 同じスコープ内に複数のTrapステートメントを書くと一番上のトラップが実行されます。
  5. 例外クラスを指定することでそのクラスに応じて処理を振り分けることができます。
  6. Trapステートメント処理は後の動作は次の3つの方法で制御可能
    1. default動作による制御
    2. breakによる制御
    3. continueによる制御

PowerShellのインタプリタでTrapはおそらくスコープ外?でトラップできなかったので今回の検証はソースをスクリプトファイル化して実行してみます。

Trapはスコープの範囲で起きた終了するエラーをすべてトラップしてしまうのできめ細やかな制御をしたい場合はtry-catchを使う事をお勧めします。

基本構文

trap {
    #例外が発生した場合に実行する処理をここに
}
trap [例外クラス] {
    #指定の例外が発生した場合に実行する処理をここに
}

コマンドレットをエラーハンドリングする

Trapステートステートでコマンドレットのエラーハンドリングを行う場合ひと手間加えないとトラップすることができません。

前述の通りTrapステートメントは終了するエラーしかトラップできないのですが、コマンドレットは終了しないエラーを発します。

なのでコマンドレットを実行する際にはエラー発生時に終了するエラーになるようにする必要があります。

その方法は2つあります。

  • コマンドレットに 「-ErrorAction Stop」を毎回つける
  • ユーザー設定変数「$ErrorActionPreference = “Stop”」を設定しておく

コマンドレットに 「-ErrorAction Stop」を毎回つける

まず1つ目の方法としてコマンドレットに必ず「-ErrorAction Stop」を付けることでそのコマンドレットがエラーとなった場合に終了するエラーを出力するようになります。

Remove-Itemコマンドレットで存在しないファイルを削除してエラーを発生させてみます。

#defaultAction.ps1 
trap {
    #例外が発生した場合に実行する処理をここに
    "トラップ成功"
}

Remove-Item aaa
PS C:\tmp> . .\defaultAction.ps1                                                                                        Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\defaultAction.ps1:7 文字:1
+ Remove-Item aaa
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

「トラップ成功」は表示されません。

次に-ErrorAction Stopを付けてみます。

#ActionStop1.ps1
trap {
    #例外が発生した場合に実行する処理をここに
    "トラップ成功"
}

Remove-Item aaa -ErrorAction Stop

6行目でErrorAction Stopを指定しています。

PS C:\tmp> . .\ActionStop1.ps1
トラップ成功
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\ActionStop1.ps1:7 文字:1
+ Remove-Item aaa -ErrorAction Stop
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

実行結果、2行目に「トラップ成功」と表示されているのでトラップができています。

ユーザー設定変数「$ErrorActionPreference = “Stop”」を設定しておく

ユーザー設定変数(preference variables)の「$ErrorActionPreference」に”Stop”をスクリプトの冒頭などで設定することでコマンドレットのデフォルトの動作を終了するエラーに変更することができます。

デフォルト値はContinueなのでそれをStopに書き換えます。

#ErrorActionPreferenceStop.ps1
$ErrorActionPreference = "Stop"
trap {
    #例外が発生した場合に実行する処理をここに
    "トラップ成功"
}

Remove-Item aaa
PS C:\tmp> .\ErrorActionPreferenceStop.ps1
トラップ成功
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\ErrorActionPreferenceStop.ps1:8 文字:1
+ Remove-Item aaa
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

「トラップ成功」が表示されたのでトラップ成功です。

Trapステートメントはスコープ内どこに記述してもOK

Trapステートメントはエラーが発生したスコープ内であればどこに記述してもトラップしてくれます。

エラー発生元より後にTrapを記述してみる

下記の例では例外が発生する2行目以降にTrapステートメントを記述しています。

#WrittenTheBottom.ps1
Remove-Item aaa -ErrorAction Stop

trap {
        #例外が発生した場合に実行する処理をここに
        "トラップ成功"
}
PS C:\tmp> . .\WrittenTheBottom.ps1
トラップ成功
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\WrittenTheBottom.ps1:2 文字:1
+ Remove-Item aaa -ErrorAction Stop
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

2行目に「トラップ成功」が出ているのでエラー発生より後ろにTrapステートメントを着てもトラップしてくれます。

functionの中で発生したエラーをfunctionの中でトラップしてみる

functionのスコープの中と外にトラップステートメントを記述して、functionの中で発生したエラーをfunctionの中でトラップしています。

#ErrInFuncTrapInFunc.ps1
trap {
        "ファンクションの外のトラップ"
}

function Func1 {
    trap {
            "Func1の中のトラップ"
    }
    Remove-Item aaa -ErrorAction Stop

}

Func1
PS C:\tmp> . .\ErrInFuncTrapInFunc.ps1
Func1の中のトラップ
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\ErrInFuncTrapInFunc.ps1:10 文字:5
+     Remove-Item aaa -ErrorAction Stop
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

2行目に「Func1の中のトラップ」と表示されました。

functionの中で発生したエラーをfunctionの外でトラップしてみる

functionの中のエラーをfunction内でトラップせずにfunctionの外でトラップしてみます。

#ErrInFuncTrapOutFunc.ps1
trap {
        "ファンクションの外のトラップ"
}

function Func1 {
    Remove-Item aaa -ErrorAction Stop
}

Func1
PS C:\tmp> . .\ErrInFuncTrapOutFunc.ps1
ファンクションの外のトラップ
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\ErrInFuncTrapOutFunc.ps1:7 文字:5
+     Remove-Item aaa -ErrorAction Stop
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

2行目で「ファンクションの外のトラップ」が表示されました。

違うスコープのfunctionのエラーはトラップできない

もちろん違うfunctionに記述したTrapステートメントではトラップできません。

#FuncInErrFuncInTrap.ps1
function Func1 {
    Remove-Item aaa -ErrorAction Stop
}

function Func2 {
    trap {
            "Func2の中のトラップ"
    }
}

Func1
PS C:\tmp> . .\OtherFuncInErrCanNotTrap.ps1                                                                             Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\OtherFuncInErrCanNotTrap.ps1:3 文字:5
+     Remove-Item aaa -ErrorAction Stop
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

「Func2の中のトラップ」は出力されません。

同じスコープ内に複数のTrapステートメントを書くと一番上のトラップが実行される

Trapステートメントを複数同じスコープ上に書いた場合は、エラーが発生した際上に書かれたTrapステートメントが適用されます。

#MultipleTrap.ps1
trap { "スクリプトの最初に書いたトラップ" } #これが実行される

trap { "エラー発生前に書いたトラップ" }

Remove-Item aaa -ErrorAction Stop #ここでエラー発生

trap { "エラー発生直後に書いたトラップ" }
PS C:\tmp> . .\MultipleTrap.ps1
スクリプトの最初に書いたトラップ
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\MultipleTrap.ps1:6 文字:1
+ Remove-Item aaa -ErrorAction Stop #ここでエラー発生
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

「スクリプトの最初に書いたトラップ」が表示されたので、最初に定義したトラップが実行されました。

例外クラス指定でTrapを振り分ける

Trapステートメントは例外クラスを指定することで処理を振り分けることができます。

trap [例外クラス] {
    #指定の例外が発生した場合に実行する処理をここに
}

ただし、どのエラーが発生した場合にどの例外クラスでトラップできるかはドキュメントがないため自分で探り充てるしかありません。例外クラスの見つけ方は前回の記事を参考にして見つけていただければと思います。

#ExceptionClassifierRm.ps1
trap [Microsoft.PowerShell.Commands.WriteErrorException] {
         write-host 'WriteError'
}
trap [System.Management.Automation.ItemNotFoundException] {
         write-host 'ItemNotFound'
}
trap [System.Management.Automation.SessionStateException] {
         write-host 'SessionState'
}
trap [System.Management.Automation.ActionPreferenceStopException] {
         write-host 'ActionPreferenceStopException' #実行される
}
trap [System.Management.Automation.RuntimeException] {
         write-host 'RuntimeException'
}
trap [System.FormatException] {
         write-host 'FormatException'
}
trap [System.Management.Automation.ErrorRecord] {
         write-host 'ErrorRecord'
}
trap [System.SystemException] {
         write-host 'SystemException'
}
trap [System.Exception] {
         write-host 'Exception'
}
trap {
         write-host 'Mmmm'
}

Remove-Item aaa -ErrorAction Stop
ActionPreferenceStopException
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\ExceptionClassifierRm.ps1:33 文字:1
+ Remove-Item aaa -ErrorAction Stop
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

「ActionPreferenceStopException」が表示され、処理を分岐できたことが分かりました。

しかしコマンドレットの場合すべてActionPreferenceStopExceptionとなってしまうので、一度Trapで捕まえた後に、switch文などで処理を振り分けたほう良いです。

Trapステートメント処理は後の動作制御の方法は3つ

Trapステートメントの処理を行った後の動作を制御する方法は次の三つがあります。

  1. default動作による制御
  2. breakによる制御
  3. continueによる制御

break文またはcontinue文をTrapのブロック内に記述することで制御を変更することができます。

動作の一覧をまとめました。

default動作による制御break文による制御continue文による制御
Trapステートメントブロック内の処理すべて実行されるbreak文まで実行されるcontinue文まで実行
標準エラー出力ありありなし
エラー発生後の処理継続する
継続しない
(上位スコープへ例外を出力する)
継続する

default動作による制御

Trapステートメントブロック内にbreak文またはcontinue文を記述しない場合の動作です。

  • Trapステートメントブロック内の処理はすべて実行されます。
  • 標準エラー出力は行われます。
  • エラー発生後の処理は継続する
#TrapDefault.ps1
function InnerFunc {
    trap {
      "InnerFunc内 Trap内 default動作" #実行される
    }
    "InnerFunc内 エラー直前" #実行される
    Remove-Item aaa -ErrorAction Stop
    "InnerFunc内 エラー直後" #実行される
}

function OuterFunc {
    trap {
      "OuterFunc内 Trap内 default動作" #実行されない
    }

    "OuterFunc内 InnerFunc 呼び出し直前" #実行される
    InnerFunc
    "OuterFunc内 InnerFunc 呼び出し直後" #実行される
}

"OuterFunc 呼び出し直前" #実行される
OuterFunc
"OuterFunc 呼び出し直後" #実行される
PS C:\tmp> . .\TrapDefault.ps1
OuterFunc 呼び出し直前
OuterFunc内 InnerFunc 呼び出し直前
InnerFunc内 エラー直前
InnerFunc内 Trap内 default動作
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\TrapDefault.ps1:7 文字:5
+     Remove-Item aaa -ErrorAction Stop
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

InnerFunc内 エラー直後
OuterFunc内 InnerFunc 呼び出し直後
OuterFunc 呼び出し直後

8行目の「InnerFunc内 エラー直後」が表示されていることから処理が継続していることが分かります。

breakによる制御

Trapステートメントブロック内にbreak文を記述した場合の動作です。

  • Trapステートメントブロック内の処理はbreak文まで実行されます。
  • 標準エラー出力は行われます。
  • エラー発生後の処理は継続しません。(スコープを抜けて上位スコープへ例外を出力します)
#TrapBreak.ps1
function InnerFunc {
    trap {
      "InnerFunc内 Trap内 break直前" #実行される
      break
      "InnerFunc内 Trap内 break直後" #実行されない
    }
    "InnerFunc内 エラー直前" #実行される
    Remove-Item aaa -ErrorAction Stop
    "InnerFunc内 エラー直後" #実行されない
}

function OuterFunc {
    trap {
      "OuterFunc内 Trap内 break直前" #実行される
      break
      "OuterFunc内 Trap内 break直後" #実行されない
    }

    "OuterFunc内 InnerFunc 呼び出し直前" #実行される
    InnerFunc
    "OuterFunc内 InnerFunc 呼び出し直後" #実行されない
}

"OuterFunc 呼び出し直前" #実行される
OuterFunc
"OuterFunc 呼び出し直後" #実行されない
PS C:\tmp> . .\TrapBreak.ps1
OuterFunc 呼び出し直前
OuterFunc内 InnerFunc 呼び出し直前
InnerFunc内 エラー直前
InnerFunc内 Trap内 break直前
OuterFunc内 Trap内 break直前
Remove-Item : パス 'C:\tmp\aaa' が存在しないため検出できません。
発生場所 C:\tmp\TrapBreak.ps1:9 文字:5
+     Remove-Item aaa -ErrorAction Stop
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\tmp\aaa:String) [Remove-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand

PS C:\tmp>

「InnerFunc内 Trap内 break直前」の後に「OuterFunc内 Trap内 break直前」が表示されていることからbreak実行後は上位スコープへ例外を出力し、上位スコープであるOuterFunc内でトラップしていることが分かります。

「InnerFunc内 エラー直後」などが表示されていないことから処理は継続されません。

ちなみにOuterFunc内 TrapをbreakにしなければOuterFunc以降の処理は継続されます。

continueによる制御

Trapステートメントブロック内にcontinue文を記述した場合の動作です。

  • Trapステートメントブロック内の処理はcontinue文まで実行されます。
  • 標準エラー出力は行われません。
  • エラー発生後の処理は継続します。
#TrapContinue.ps1
function InnerFunc {
    trap {
      "InnerFunc内 Trap内 continue直前" #実行される
      continue
      "InnerFunc内 Trap内 continue直後" #実行されない
    }
    "InnerFunc内 エラー直前" #実行される
    Remove-Item aaa -ErrorAction Stop
    "InnerFunc内 エラー直後" #実行される
}

function OuterFunc {
    trap {
      "OuterFunc内 Trap内 continue直前" #実行されない
      continue
      "OuterFunc内 Trap内 continue直後" #実行されない
    }

    "OuterFunc内 InnerFunc 呼び出し直前" #実行される
    InnerFunc
    "OuterFunc内 InnerFunc 呼び出し直後" #実行される
}

"OuterFunc 呼び出し直前" #実行される
OuterFunc
"OuterFunc 呼び出し直後" #実行される
PS C:\tmp> . .\TrapContinue.ps1
OuterFunc 呼び出し直前
OuterFunc内 InnerFunc 呼び出し直前
InnerFunc内 エラー直前
InnerFunc内 Trap内 continue直前
InnerFunc内 エラー直後
OuterFunc内 InnerFunc 呼び出し直後
OuterFunc 呼び出し直後
PS C:\tmp>

「InnerFunc内 エラー直後」が表示されているので処理が継続していることが分かります。また、「OuterFunc内 Trap内 continue直前」が表示されていないことからも上位へ例外を出力していないことが分かります。

標準エラーへも表示されていないので出力されません。(ただし$Error変数には値が設定されています。)

まとめ

今回は前回のTry-Catchに引き続いてエラーを捕捉する仕組みであるTrapについて検証しながらその仕様を解説してみました。

TrapはTry-Catchに比べてあまり使い勝手がよくないのでおそらく使う頻度は少ないかと思いますが、スコープ内をひとまとめにすることができたり、またネストを浅くすることができます。(Try-Catchはネストが深くなって少し読みにくいです。)

改めて仕様をまとめるておきます。

  1. トラップできるのは終了するエラー
  2. TrapはTrapステートメントを書いたスコープで発生した終了するエラーをトラップする。
  3. Trapステートメントはスコープ内どこに記述してもOK
  4. 同じスコープ内に複数のTrapステートメントを書くと一番上のトラップが実行される。
  5. 例外クラスを指定することでそのクラスに応じて処理を振り分けることができる。
  6. Trapステートメント処理は後の動作は次の3つの方法で制御可能
    1. default動作による制御
    2. breakによる制御
    3. continueによる制御
default動作による制御break文による制御continue文による制御
Trapステートメントブロック内の処理すべて実行されるbreak文まで実行されるcontinue文まで実行
標準エラー出力ありありなし
エラー発生後の処理継続する
継続しない
(上位スコープへ例外を出力する)
継続する

のこるPowerShellにおけるエラーハンドリングの方法については次回以降も引き続き書いていきたいと思います。

最後までご覧いただきありがとうございました。

以上、やましー@データ活用クラウドエンジニア(@yamashi18041)でした。