trap制御構文

PowerShell Coreにはエラーを捕捉する機能としてtry-catch制御構文が用意されていることを取り上げた。また、throw文を使うことで明示的にエラー(例外)を発生させられることも説明した。

PowerShellにはtry-catch制御構文と似たような機能としてtrap制御構文が用意されている。try-catch制御構文はtry {}で対象を囲み、catch {}およびfinally {}にエラーが発生したときの処理を記述する。trap制御構文はtry {}のないtry-catch制御構文のようなもので、次のようにtrap {}だけで構成されている。

trap制御構文の基本的な使い方

trap { 処理 }

try-catch制御構文のcatchでは捕捉するエラーを個別に指定することができたが、trapでも同じような記述で捕捉するエラーを指定することができる。

trap制御構文の基本的な使い方 捕捉対象を指定する場合

trap [エラー] { 処理 }

trap制御構文もPowerShell Coreのシェルスクリプトで使われているので、使い方や動作の内容を理解しておく必要がある。以下、この制御構文について解説する。

trapの実行例

もっとも基本的なtrapの使用例を次に掲載する。

trapの基本的な使い方

function FooBar {
    trap { "エラーを捕捉しました" }

    "エラー発生前"
    1 / 0       # エラー発生
    "エラー発生後"
}

FooBar

実行すると次のようになる。

trapでエラーを検出して、処理を継続

PS /Users/daichi> function FooBar {
>> trap { "エラーを捕捉しました" }
>>
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
エラーを捕捉しました
Attempted to divide by zero.
At line:5 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

エラー発生後
PS /Users/daichi>

数値を0で割るとSystem.DivideByZeroExceptionというエラー(例外)が発生するのだが、このエラーが発生した段階で処理がtrap {}に飛んでいることがわかる。そしてtrap {}の処理が終わると、エラーが発生していた場所に戻って処理が継続されていることがわかる。

ためしに、trap {}抜いて先ほどのコードを実行すると、次のようになる。

trapを使わなかった時の処理の流れ

function FooBar {
    "エラー発生前"
    1 / 0       # エラー発生
    "エラー発生後"
}

FooBar

エラーが発生しても処理は継続

PS /Users/daichi> function FooBar {
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
Attempted to divide by zero.
At line:3 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

エラー発生後
PS /Users/daichi>

trap {}が記載されていないのでtrap {}に処理が飛ぶことはないが、それ以外の処理の流れは同じだ。エラーが発生しても次の行へ処理が進んでいる。

この処理の流れはbreakを指定すると変更することができる。次のようにtrap {}の中でbreakを指定すると、そこで処理が止まるようになる。このあたりのbreakの意味合いは繰り返し構文の中のbreakとよく似ている。

trapをbreakで終了した場合

function FooBar {
    trap { "エラーを捕捉しました"; break}

    "エラー発生前"
    1 / 0       # エラー発生
    "エラー発生後"
}

FooBar

breakが指定されていると処理が戻らない

PS /Users/daichi> function FooBar {
>> trap { "エラーを捕捉しました"; break}
>>
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
エラーを捕捉しました
Attempted to divide by zero.
At line:5 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : RuntimeException

PS /Users/daichi>

これがtrap {}の基本的な使い方だ。trap {}を使うことでエラーが発生した場合に処理を実行することができ、そのまま処理を継続させたり、たとえば発生したエラーの詳細情報を表示させたりといったことを手軽に実施することができる。

捕捉対象を限定する

catchのように捕捉するエラーの対象を指定して絞り込むことができる。たとえば、次のように何も指定しない場合と、ドンピシャのエラーであるSystem.DivideByZeroExceptionを指定する場合の2つを書いてみる。

エラー(例外)の種類を指定したtrapも設置

function FooBar {
    trap { "エラーを捕捉しました" }
    trap [System.DivideByZeroException] { "0割りエラーを捕捉しました" }

    "エラー発生前"
    1 / 0       # エラー発生
    "エラー発生後"
}

FooBar

これを実行すると次のようになる。

該当する方のtrap {}が実行されている

PS /Users/daichi> function FooBar {
>> trap { "エラーを捕捉しました" }
>> trap [System.DivideByZeroException] { "0割りエラーを捕捉しました" }
>>
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
0割りエラーを捕捉しました
Attempted to divide by zero.
At line:6 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

エラー発生後
PS /Users/daichi>

何も指定していないtrap {}ではなく、捕捉するエラーを指定した方のtrap {}が実行されていることがわかる。

ためしに、関連するエラーを使って次のようにtrapをたくさん書いてみる。

捕捉対象を増やしてみる

function FooBar {
    trap { "エラーを捕捉しました" }
    trap [System.DivideByZeroException] { "0割りエラーを捕捉しました" }
    trap [System.ArithmeticException] { "数値演算エラーを捕捉しました" }
    trap [System.SystemException] { "システムエラーを捕捉しました" }
    trap [System.Exception] { "エラーを捕捉しました" }

    "エラー発生前"
    1 / 0       # エラー発生
    "エラー発生後"
}

FooBar

実行すると次のようにドンピシャなエラーだけが実行されていることがわかる。

もっとも適切に該当するものが使われている

PS /Users/daichi> function FooBar {
>> trap { "エラーを捕捉しました" }
>> trap [System.DivideByZeroException] { "0割りエラーを捕捉しました" }
>> trap [System.ArithmeticException] { "数値演算エラーを捕捉しました" }
>> trap [System.SystemException] { "システムエラーを捕捉しました" }
>> trap [System.Exception] { "エラーを捕捉しました" }
>>
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
0割りエラーを捕捉しました
Attempted to divide by zero.
At line:9 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

エラー発生後
PS /Users/daichi>

ドンピシャで一致するSystem.DivideByZeroExceptionを除いてみる。

ドンピシャの捕捉対象だけ抜いてみる

function FooBar {
    trap { "エラーを捕捉しました" }
    trap [System.ArithmeticException] { "数値演算エラーを捕捉しました" }
    trap [System.SystemException] { "システムエラーを捕捉しました" }
    trap [System.Exception] { "エラーを捕捉しました" }

    "エラー発生前"
    1 / 0       # エラー発生
    "エラー発生後"
}

FooBar

これを実行すると次のようにSystem.ArithmeticExceptionを指定したtrap {}が実行されていることがわかる。

その親のエラーで捕捉されている

PS /Users/daichi> function FooBar {
>> trap { "エラーを捕捉しました" }
>> trap [System.ArithmeticException] { "数値演算エラーを捕捉しました" }
>> trap [System.SystemException] { "システムエラーを捕捉しました" }
>> trap [System.Exception] { "エラーを捕捉しました" }
>>
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
数値演算エラーを捕捉しました
Attempted to divide by zero.
At line:8 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

エラー発生後
PS /Users/daichi>

System.DivideByZeroExceptionは親がSystem.ArithmeticExceptionだ。System.ArithmeticExceptionの親はSystem.SystemExceptionで、System.SystemExceptionの親がSystem.Exceptionだ。つまり、捕捉対象としてもっとも正確というか、もっとも近しいところを選んでtrap {}が実行されていることがわかる。

try-cath制御構文とtrap制御構文の関係

同じようなことができるtry-catch制御構文とtrap制御構文だが、こうなってくると組み合わせて使った場合にどうなるのかが気になるところだ。どのように動作するのか組み合わせたものを実行して調べてみよう。

まず、次のようにtry-catchの外にtrapを仕掛けてみる。

trapとtry-catchを組み合わせてみる

function FooBar {
    trap [System.DivideByZeroException] {
        "trap: 0割りエラーを捕捉しました"
    }

    try {
        "エラー発生前"
        1 / 0       # エラー発生
        "エラー発生後"
    } catch [System.DivideByZeroException] {
        "catch: 0割りエラーを捕捉しました"
    } finally {
        "finallyに入りました"
    }
}

FooBar

実行すると次のようになる。

trapではなくcatchが機能している

PS /Users/daichi> function FooBar {
>> trap [System.DivideByZeroException] {
>> "trap: 0割りエラーを捕捉しました"
>> }
>>
>> try {
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> } catch [System.DivideByZeroException] {
>> "catch: 0割りエラーを捕捉しました"
>> } finally {
>> "finallyに入りました"
>> }
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
catch: 0割りエラーを捕捉しました
finallyに入りました
PS /Users/daichi>

エラーが発生するとcatchとfinallyが実行されていることがわかる。try {}の外に書いたtrapは捕捉の仕組みとして使われていないことがわかる。

次に、trap {}をtry {}の中に書いてみる。

trapとtry-catchを組み合わせてみる その2

function FooBar {
    try {
        trap [System.DivideByZeroException] {
            "trap: 0割りエラーを捕捉しました"
        }
        "エラー発生前"
        1 / 0       # エラー発生
        "エラー発生後"
    } catch [System.DivideByZeroException] {
        "catch: 0割りエラーを捕捉しました"
    } finally {
        "finallyに入りました"
    }
}

FooBar

実行すると次のようになる。

catchではなくtrapが機能している

PS /Users/daichi> function FooBar {
>> try {
>> trap [System.DivideByZeroException] {
>> "trap: 0割りエラーを捕捉しました"
>> }
>> "エラー発生前"
>> 1 / 0 # エラー発生
>> "エラー発生後"
>> } catch [System.DivideByZeroException] {
>> "catch: 0割りエラーを捕捉しました"
>> } finally {
>> "finallyに入りました"
>> }
>> }
PS /Users/daichi>
PS /Users/daichi> FooBar
エラー発生前
trap: 0割りエラーを捕捉しました
Attempted to divide by zero.
At line:7 char:1
+ 1 / 0 # エラー発生
+ ~~~~~
+ CategoryInfo          : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

エラー発生後
finallyに入りました
PS /Users/daichi>

今度はcatch {}が機能せずにtrap {}が機能していることがわかる。書く場所によってその影響範囲が変わっているわけだ。

try-catch制御構文とtrap制御構文

try-catch制御構文とtrap制御構文のどっちを使うべきかはなかなか悩ましい問題だ。結局書き方を変えればどちらも同じようなことができてしまう。あとは書き手の好みの問題だ。

try-catch制御構文と比べて、trap制御構文は1行だけ追加すればやりたいことができてしまう(たとえばエラーメッセージを出すとか)ので、少ない行数で事を済ませたいという場合にはtrap制御構文の方が扱いやすいかもしれない。どちらを使うかは好みの問題だが、混ぜるとどちらが優先的に動作するのかわかりにくくなるので、それは避けたほうがよいだろう。

参考資料