鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

パラメーターの検証属性について/おまけ

本編に入れられなかった微妙なトピックをいくつか解説します。
三部作の最後を締めくくるにふさわしくない、雑多な記事ですが、ご勘弁ください。

配列型のデフォルト値

Mandatory でない配列型パラメーターには、デフォルト値として空の配列を指定しておくとよいのではないかと思います。
つまり、こう。

デフォルト値を指定していない場合、パラメーター x を指定せずに呼び出すと、$x には $null が入っています。
これをループ処理すると、$null だけが入った要素数 1 の配列として、1 回処理が回ってしまいます。

パラメーターが指定されたかどうかを $PSBoundParameters を使って判定することは可能ですが、この場合なら、デフォルト値を使った方が綺麗だと思います。

AutomationNull

後編で少し触れた AutomationNull についてです。
ドキュメントによると、

Indicates a void return result.

とか、

Any operation that returns no actual value should return AutomationNull.Value.

と書いてありますので、ひょっとすると、

$x = Write-Host Hoge

のように、戻り値を返さないコマンドの結果のようなところで使われるのだろうかと思うのですが、確認できていません。
上記の $x は、

$x | gm

とすると、

gm : Get-Member コマンドレットにオブジェクトを指定する必要があります。

というエラーになります。
これは、

$null | gm

と同じ挙動なのですが、しかし、実は $x は $null ではありません。
というのは、以下のようにして確認することができます。

@($x).Length
@($null).Length

前者は 0、後者は 1 になることから、$x と $null は全く同じものではないということがわかります。
さらに、

@([System.Management.Automation.Internal.AutomationNull]::Value).Length

も 0 になります。
このような挙動から、おそらく $x は AutomationNull.Value であり、それは、$null とは異なる、「何も無い」状態を表すものではないかと思っています。

配列の扱い

Mandatory 指定したパラメーターは、$null や空文字が禁止されるほか、それらを含む配列も禁止されます。
空の配列も禁止されますが、実は、「空の配列を含む配列」は禁止されないのです。
とは言っても、@(@()) のように、単に配列を入れ子にしただけでは、簡略化されて @() になってしまいます。
「空の配列を含む配列」を作るためには、例えば @(@(), @()) のようにする必要があります。

さて、空の配列を含む配列は何故禁止されないのでしょうか?

PowerShell において、パラメーターに配列を指定するということは、その個々の要素を指定してコマンドを複数回呼ぶことと等価だと思います。
例えば、

PS > Get-Command Get-Content, Get-Help

というのは、

PS > Get-Command Get-Content
PS > Get-Command Get-Help

というのと、概ね等価だと考えてよいと思います。

ですから、単に空の配列を指定することは、「何もコマンドを呼ばない」ことと等しいのでしょう。

しかし、では、空の配列を渡された Get-Command は何をしたらいいでしょう?
それはあたかも、「1÷0」のごときもので、エラーにするしかないのではないかと思います。

Mandatory が空の配列をエラーにするというのは、そのような状態を禁止するための挙動なのでしょう。
そして、配列がこのような「複数のコマンドを一括して呼び出す」ように振る舞うのは、その最外周に限られます。
入れ子になった内部の配列は、そのような意味を持たない、単なる「配列オブジェクト」なので、チェックされないのだろうと思われます。

Mandatory なパラメーターを指定しなかった場合の挙動

ちょっと細かすぎて伝わらない系の話です。

Mandatory なパラメーターを指定せずにコマンドを呼び出そうとすると、その場で入力を促されますが、この挙動を前提としたコマンド設計はしない方がいいと思います。
何のことかと言いますと、この入力は機能的にかなり貧弱でして、文字列型か、文字列型から単純に変換できる [int] などの型、もしくはそれらの配列しか受け付けません。
例えば、[hashtable] 型のパラメーターで入力を促されて "@{X = 1}" とか入力してもエラーになります。コマンドも呼び出せませんし、変数も(ごく一部を除いて)参照できません。

ということは、です。
「Mandatory を指定すると、貧弱な入力プロンプトが出る」ということを前提に考えると、「Mandatory 指定するのは、貧弱なプロンプトで受け付けられる型のみにすべき」という考えが出て…来ないことも、ありません、かね?
やめた方がいいというのは、この考えのことです。

このプロンプトは、言わば powershell.exe (と ISE)が提供しているオマケ機能、言ってしまえば「おせっかい」みたいなものです。
事実、スクリプトpowershell.exe 上で実行する以外に、C# 等で書いたアプリ内で実行することもできますが、そうした場合はプロンプトは出ず、単に例外が発生します。
Mandatory は、必須ならばとにかく指定すべきで、それをどう扱うかはコマンドの関与することではないという認識が良いと思います。

検証に失敗した時の例外

パラメーターの検証に失敗すると例外が投げられます。
この例外の情報は、$Error[0] を見ればわかるのですが、例外の型は System.Management.Automation.ParameterBindingValidationException となっています(Validate 系属性の場合は InnerException プロパティに ValidationMetadataException が入っています。Mandatory の場合は InnerException はありません)。
で、この ParameterBindingValidationException、internal なクラスでして、外部からアクセスできないんですね。
基底クラスの ParameterBindingException なら public なんですが…。

PowerShellスクリプトを書く上では、この例外をキャッチしてどうこうするということはまずないでしょうから、internal でも大した影響はないのですが、C# 等のコードからスクリプトを呼び出す場合は、型を指定してキャッチしたい場合もあります。

型が隠されている以上、普通の方法では解決法はありません。
基底クラスでキャッチして、リフレクションでどうにかするしかないでしょう。

Connect にもフィードバックが投稿されていたので Vote しておきました。