PowerShell で try – catch – finally

# この記事は以前 C#と諸々 に書いた記事 (を一部修正したもの) です。

 

PowerShell V1.0 では、C# の try – catch – finally のような構造化例外処理がサポートされていません。しかし、trap ブロック、throw ステートメント、break ステートメント、continue ステートメント、そしてスコープについて理解すれば、try – catch – finally を擬似的に再現することができます。

というわけで、次のコードのように C# の try – catch – finally に近い例外処理を実現する関数を書きました。

try {
    # 処理
} catch ([例外の型]) {
    param($ex)
    # 例外処理
} finally {
    # 後処理
}

使用する際は次の点に気を付けてください。

  • 各ブロックの開始の "{" の前と各ブロックの終了の "}" の後ろは、上記のように、改行せずに記述する必要があります。
  • 例外の型は、上記のように、必ず "()" で囲む必要があります。
  • catch または finally は、省略可能です。例外の型も省略可能です。
  • catch ブロック内では、break ステートメントを使用して例外を再スローすることができます。当然、任意の例外を throw ステートメントでスローすることもできます。

で、これらを実現するための関数がこちらです。

function global:try
{
    $currentArgIndex = 0;
    $tryBlock = $args[$currentArgIndex];
    $currentArgIndex++;
    if ($tryBlock -isnot [System.Management.Automation.ScriptBlock])
    {
        throw New-Object "ArgumentException" @("try ブロックの指定が不正です。");
    }
    if ("catch" -eq $args[$currentArgIndex])
    {
        $currentArgIndex++;
        if ($args[$currentArgIndex] -is [Type])
        {
            $targetExceptionType = $args[$currentArgIndex];
            $currentArgIndex++;
        }
        $catchBlock = $args[$currentArgIndex];
        $currentArgIndex++;
        if ($catchBlock -isnot [System.Management.Automation.ScriptBlock])
        {
            throw New-Object "ArgumentException" @("catch ブロックの指定が不正です。");
        }
    }
    if ("finally" -eq $args[$currentArgIndex])
    {
        $currentArgIndex++;
        $finallyBlock = $args[$currentArgIndex];
        $currentArgIndex++;
        if ($finallyBlock -isnot [System.Management.Automation.ScriptBlock])
        {
            throw New-Object "ArgumentException" @("finally ブロックの指定が不正です。");;
        }
    }
    if (($() -eq $catchBlock) -and ($() -eq $finallyBlock))
    {
        throw New-Object "ArgumentException" @("catch ブロックまたは finally ブロックを指定してください。");
    }
    &{
        $requireFinally = ($() -ne $finallyBlock);
        &{
            &$tryBlock;
            trap
            {
                if ($() -eq $catchBlock)
                {
                    break;
                }
                $ex = $_.Exception;
                if (($() -ne $targetExceptionType) -and (!$targetExceptionType.IsAssignableFrom($ex.GetType())))
                {
                    break;
                }
                &$catchBlock $ex;
                continue;
            }
        };
        if ($requireFinally)
        {
            $requireFinally = $False;
            &$finallyBlock;
        }
        trap
        {
            if ($requireFinally)
            {
                $requireFinally = $False;
                &$finallyBlock;
            }
            break;
        }
    };
}

以下のスクリプトを実行すると、動作が確認できます。

try {
    "try ブロック実行";
    throw New-Object "ArgumentException";
    "この文は出力されない";
} catch ([ArgumentException]) {
    param ($ex)
    "{0} がスローされたから catch ブロック実行" -f $ex.GetType().Name;
} finally {
    "finally ブロック実行";
}

出力は以下のようになります。

try ブロック実行
ArgumentException がスローされたから catch ブロック実行
finally ブロック実行

なお、PowerShell V2.0 では、try – catch – finally が標準でサポートされるようです。なので、V2.0 ではこの関数は不要になるのですが、もし V1.0 で try – catch – finally を使用したいという方はぜひこの関数を試してみてください。

FizzBuzz 問題

FizzBuzz 問題をご存じでしょうか?

一時期、ネット上で話題となった問題です。

恐らくこの記事がきっかけだったかと思います。

問題は次のような内容です。

1 から 100 までを順番に出力しなさい。

ただし、3 の倍数の時は数字の代わりに Fizz を、

5 の倍数の時は数字の代わりに Buzz を、

3 の倍数且つ 5 の倍数の時は数字の代わりに FizzBuzz を出力しなさい。

また、一部の人達の間では、できるだけコードの文字数を短縮することが競われました。

というわけで、この問題を PowerShell でできるだけ少ない文字数で解いてみましょう。

と、その前に。

本記事では、次のコードが理解できることを前提条件とさせて頂きます。

このコードは、FizzBuzz 問題を文字数にこだわらずに PowerShell で解いた一例です。

1..100 |

    % {

        $s = “”;

        if (($_ % 3) -eq 0)

        {

            $s += “Fizz”;

        }

        if (($_ % 5) -eq 0)

        {

            $s += “Buzz”;

        }

        if ($s -eq “”)

        {

            Write-Host $_;

        }

        else

        {

            Write-Host $s;

        }

    };

よろしいでしょうか。

では、いきなり僕の中での最短コードをお見せします。

1..100|%{$s=@(“Fizz”)[$_%3]+@(“Buzz”)[$_%5];($s,$_)[!$s]}

57 文字です。

このコードを PowerShell で実行してみれば、ちゃんと FizzBuzz になっていることがおわかり頂けると思います。

このコードはどういう理屈で FizzBuzz してるのでしょうか?

このままじゃ見にくいので、このコードにスペースと改行を混ぜ、更にコードスタイルを統一することで、もう少し見やすいコードにしてみましょう。

@(1..100) |

    % {

        $s = @(“Fizz”)[$_ % 3] + @(“Buzz”)[$_ % 5];

        @($s, $_)[!$s];

    };

これで見た目は大分わかりやすくなりましたが、3 行目と 4 行目が何をしているのかわかりますか?

3 行目は、3 の倍数なら Fizz、5 の倍数なら Buzz、3 の倍数且つ 5 の倍数なら FizzBuzz を、変数 s に代入する、というコードです。

4 行目は、変数 s が null でなければ変数 s を出力し、変数 s が null ならパイプラインに渡された数値を出力する、というコードです。

どちらもポイントは「配列の添え字」です。

まず、3 行目の @(“Fizz”)[$_ % 3] を見てみます。

“Fizz” という一つの要素を持つ配列を作成し、パイプラインに渡された数値を 3 で割った時の剰余を、配列の添え字に指定しています。

すると、パイプラインに渡された数値が 3 の倍数の場合、添え字は 0 になります。つまり、配列の 0 番目の要素である “Fizz” が取得されます。

そして、それ以外の場合、添え字は 1 か 2 になります。この配列には 1 つしか要素がないので、配列の要素数を超えてしまうことになるのですが、これは PowerShell ではエラーにならず、null が取得されます。

つまり @(“Fizz”)[$_ % 3] は、パイプラインに渡された数値が 3 の倍数なら “Fizz” を返し、それ以外は null を返すのです。

同じく、@(“Buzz”)[$_ % 5] は、パイプラインに渡された数値が 5 の倍数なら “Buzz” を返し、それ以外は null を返します。

更に、この二つは + で繋がれているので、3 の倍数且つ 5 の倍数の時は “FizzBuzz” を返すというわけです。

ちなみに + 演算子の左右が null の場合は null になりますので、3 の倍数でも 5 の倍数でもない場合は null を返します。

次に、4 行目の @($s, $_)[!$s] を見てみます。

変数 s と、パイプラインに渡された数値の、二つの要素を持つ配列を作成し、変数 s を論理否定した値を、配列の添え字に指定しています。

「変数 s を論理否定した値を、配列の添え字に指定」というのが意味不明ですね。まず「変数 s を論理否定した値」 (!$s の部分) だけを見てみましょう。

実は、PowerShell では Boolean 値に限らずどんなオブジェクトにでも、論理否定演算子を使用することができます。その際、オブジェクトは一度暗黙的に Boolean 値に変換されてから論理否定が行われます。そして、文字列を Boolean 値に変換する場合、文字列の長さが 1 以上で “false” という文字列でない場合は true、それ以外の場合 (文字列の長さが 0 である、または “false” という文字列である、または null である場合) は false を返します。

ここでは、変数 s には null か “Fizz”“Buzz”“FizzBuzz” しか入りません。つまり、!$s は変数 s が null なら true、それ以外は false を返します。

「変数 s を論理否定した値」の意味がわかりましたので、「変数 s を論理否定した値を、配列の添え字に指定」について見てみましょう。

といっても、後は Boolean 値を添え字にした場合の挙動がわかればいいだけの話です。

Boolean 値を添え字に指定した場合、true なら 1 に、 false なら 0 になります。

つまり、変数 s が null なら 1、それ以外は 0 を返すというわけです。

配列の 0 番目は変数 s、1 番目はパイプラインに渡された数値ですので、先ほど書いたように、変数 s が null でなければ変数 s を出力し、変数 s が null ならパイプラインに渡された数値を出力する、ということになるわけです。

以上で説明終了です。

ちなみに、この最短コードはあくまで僕の中での最短コードです。もしかしたら、これよりもっと短くできるかもしれませんし、できないかもしれません。

それでは。

親スコープの変数を設定

次のコードを実行してみてください。

function Hoge
{
  $a = 0;
  &{
    $a = 1;
  };
  return $a;
}

Hoge;

このコードでは Hoge 関数の中の子スコープで変数 a に 1 を代入しています。
子スコープで変数に値を代入しても親スコープの変数には影響しません。
よって、Hoge 関数の戻り値は 0 です。

では次のコードを実行してみてください。

function Hoge
{
  $a = 0;
  &{
    Set-Variable -Name "a" -Value 1 -Scope 1;
  };
  return $a;
}

Hoge;

このコードを実行すると 1 が返ってきます。
子スコープから親スコープの変数に値を代入したということです。

Set-Variable コマンドレットでは、設定する変数のスコープを、Scope オプションにて指定することができます。
そして、Scope オプションに数値を指定した場合、現在のスコープからの相対的な位置として解釈します。
つまり、Scope オプションに 1 を指定すると、親スコープの変数を設定できるわけです。

だから何だ?と言われると何とも言えないのですが、マニアックなことやろうとした時、これが思わぬ効果を発揮するかもしれません。

列挙値は文字列で指定できる

列挙値は、文字列で列挙値の名前を指定することができます。

例えば Console の背景色を DarkRed に変更する、次のようなコードは、

[Console]::BackgroundColor = [ConsoleColor]::DarkRed;

次のコードのように指定することもできます。

[Console]::BackgroundColor = "DarkRed"; 

 

また、ビットフィールドの列挙値の組み合わせも、各列挙値をカンマ区切りで繋げた文字列で指定することができます。

例えば、Object クラスに定義されている非公開のインスタンスメソッドを取得する、次のようなコードは、

[Object].GetMethods([System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance) | % { $_.ToString(); };

次のコードのように指定することもできます。

[Object].GetMethods("Public, NonPublic, Instance") | % { $_.ToString(); };

 

見ての通り、列挙値を文字列で指定すると文字数がだいぶ節約できます。

ただし、オーバーロードの解決が期待通りにされない場合もあるので注意です。

例えば、Object クラスに定義されている非公開の FieldGetter メソッドを取得する場合、次のようなコードではオーバーロードの解決が期待通りになりません。

[Object].GetMethod("FieldGetter", "NonPublic, Instance") | % { $_.ToString(); };

この場合、次のコードのように明示的にキャストを行う必要があります。

[Object].GetMethod("FieldGetter", [System.Reflection.BindingFlags]"NonPublic, Instance") | % { $_.ToString(); };

このことにさえ気を付けていれば、列挙値の指定は文字列で行った方が楽だし可読性にも優れていると思います。

宜しくお願いします

Author のよこけんです。

「PowerShell はシステム管理者のためのツール」というイメージが強いかと思いますが、実は .NET 開発者を強力にサポートしてくれるツールでもあります。

実際、僕は C# プログラマですが PowerShell にはいつも開発をサポートして貰っています。

ということで、僕個人としては開発者の方々に PowerShell が普及して欲しいと思っています (僕個人の考えであって、PowerShell from Japan!! の主旨とかではありません) 。

まぁなにはともあれ、これから宜しくお願いします。

 

 

# ちなみにC#と諸々という個人ブログやってます。こちらもよろしくです~!