はい。ゴールデンウィークも気付けば残り1日。11連休取ったのに、半分以上を寝て過ごしてました。ご機嫌いかがでしょうか。
続き物にすると後半が書かれないブログとして定評を得てしまうのも困りますし、そろそろやる気を出しましょう。
というわけで、だいぶ間が空いてしまいましたが後半です。
前編では、パラメーターの検証属性についてまとめてみました。
後編では、Validate 系の検証属性を自作してみます。
観察
まず、Validate 系属性のクラス階層を見てみましょう。こうなっています。
- System.Object
- System.Attribute
- System.Management.Automation.Internal.CmdletMetadataAttribute
- System.Attribute
CmdletMetadataAttribute は PowerShell で使用される様々な属性の基底クラスになっていますし、名前から言っても、Validate 系属性ではありません。
Validate 系属性を自作する上では、ValidateArgumentsAttribute が、継承すべき基底クラスとなりそうです。
英語ですが、一応ドキュメントを読んでみましょう。
Validate メソッドをオーバーライドして、検証に失敗したら ValidationMetadataException をスローすればよさそうだということがわかります。
もうひとつ、気になるクラスがあります。ValidateEnumeratedArgumentsAttribute です。
こっちは、配列型の引数が渡された場合に、その個々の要素を検証するのを助けてくれるクラスだと言えそうです。
ただ、このクラスの概要ドキュメントには誤りがあります。
Remarks のセクションでは Validate メソッドをオーバーライドしろと書いてありますが、それではこのクラスの意味がありません。このクラスから派生するなら ValidateElement をオーバーライドしなければなりません。Microsoft 仕事しろ。
なお、配列でない値が渡された場合も、ちゃんとその引数を ValidateElement に渡してくれます。
標準で用意されているクラスについて見てみると、ValidateArgumentsAttribute から直接派生しているのは、
の3つです。
対して、ValidateEnumeratedArgumentsAttribute から派生しているのは、
- ValidateLengthAttribute
- ValidatePatternAttribute
- ValidateRangeAttribute
- ValidateScriptAttribute
- ValidateSetAttribute
の5つです。
こうして見ると、何となく違いが見えて来ないでしょうか。
どうやら、配列が渡された場合に、配列自体について(も)チェックするものは前者、配列の中身についてだけチェックするものは後者という使い分けで良さそうな気がします。
なお、Allow 系の属性は CmdletMetadataAttribute から直接派生しており、Allow 系に共通の基底クラスがないことと、オーバーライドすべき適切なメソッドが見当たらないことから、自作は不可能と思われます。
作ってみる属性
今回、2つの属性を作ってみます。
ValidateNotEmptyStringAttribute
文字列型の値が空文字列でないかどうかをチェックする属性です。
ValidatePathAttribute
パス文字列を取るパラメーターで、そのパスの実在性をチェックする属性です。
ValidateNotEmptyStringAttribute
文字列型の配列を受け取るコマンドを作ることを考えてみましょう。
空文字列は受け入れたくないけれど、空の配列は構わないというケースは想定できます。コマンド内でループ処理するなら、空の配列は結局処理されないので、渡されないのと同じことです。
しかし、この場合に ValidateNotNullOrEmptyAttribute をつけてしまうと、コマンド側はいいのですが、呼び出し側が面倒です。配列が空かどうかを判別して、そのパラメーターを指定するかしないかで呼び分けなければいけません。
つまり、こういう想定です。
これ↑を、こう↓したいわけです。
実のところ、こう↓
でもいいんですけど、[int]::MaxValue というのが何だか気持ち悪いので…。
ただ、この記事を書きながら考え直してみると、やはり自作せずに済むものは作らないに越したことはないので、[ValidateLength()] で済ませるべきなんでしょうね…。
まぁ、今回は内容はともかく、Validate 系属性が自作できるということを示すことに意義がある記事ですので、良しとしましょう。してください。お願い。
コード
というわけでコード全文です。
ポイントは
- ValidateEnumeratedArgumentsAttribute から派生し、ValidateElement をオーバーライドする。
- 検証に失敗した時は ValidationMetadataException をスローする。
- [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] をつける。これは、コマンドのパラメーターは、C# でコマンドを記述する場合、コマンド クラスのプロパティまたはフィールドになるためです。
といったところでしょうか。
具体的な実装の中身については記事の趣旨上、あまり重要ではないのですが、一応、解説しておきます。
まず、null かどうかをチェックします。
前回も冒頭で触れましたが、PowerShell では string 型に $null を代入できません。
そのため、クラス名は ValidateNotNullOrEmptyString ではなく ValidateNotEmptyString としています。
とはいえ、ValidateElement メソッドは型変換の前に呼び出されますので、element 引数には null が渡されることがあり得ます。
as string で変換する前に null チェックをしているのは、as で変換した結果が null だった場合、元々 null だったのか、型が string でなかったために null になったのかが区別できないためです。
それを区別しない場合、例外メッセージは「引数が null か、または string ではありません」といったものになりますが、このメッセージはわかりにくいと思ったため、元々 null だった場合を先に判定しているわけです。
AutomationNull.Value というのは、率直に言って、よくわかりません。
が、null と同じように扱うのがお作法みたいなのでそうしています。
これについては、この後の「おまけ」で少し解説します。
あとは、string 型でなかった場合と、空文字列だった場合の判定を行っています。
ちなみに、ValidateArgumentsAttribute クラスのドキュメント(ValidateEnumeratedArgumentsAttribute でも同じ)では、ToString メソッドもオーバーライドすることが推奨されているため、適当に実装しています。
が、なんと、標準の Validate 系属性クラスは、一つとして ToString メソッドをオーバーライドしているものがありません。Microsoft 仕事しろ。
ValidatePathAttribute
パス文字列を取るパラメーターで、そのパスの実在性をチェックする属性です。
どうして標準で用意されていないんだと強く思います。
が、先日、valentia のコードを眺めていたら、
でいいんだということに気が付きました。
というわけで、早速要らない子ですが、Validate 系属性が自作できるということを示すことに意義が(以下略
コード
注意点は ValidateNotEmptyStringAttribute の方も参照してください。
PathType プロパティを設けることで、ファイル パスのみとか、ディレクトリ パスのみといった検証も可能にしています。
ValidateNotEmptyStringAttribute 同様、配列の場合は中身を検証したいだけで、配列自体を検証する属性ではありません。
が、こちらは ValidateEnumeratedArgumentsAttribute ではなく ValidateArgumentsAttribute から直接派生しています。
何故かと言うと、ValidateEnumeratedArgumentsAttribute.ValidateElement では、ValidateArgumentsAttribute.Validate にはある engineIntrinsics というパラメーターが渡されないからです。
パスの有無を検証するにはどうしたらよいでしょうか。
Test-Path や Resolve-Path コマンドを呼ぶという手もあるでしょう。しかし、C# のコードから PowerShell のコマンドを叩くのは、可能ですが面倒です。もうちょっといい方法はないでしょうか。
File.Exists や Directory.Exists を使うのはダメダメです。それでは HKLM: 以下のパスなどで機能しないからです。
そこで目を付けたのが EngineIntrinsics です。これはスクリプトの場合は $ExecutionContext にセットされているものと同じで、このメンバーを手繰っていくと、PowerShell のかなり広範な情報にアクセスすることができます。
例えば、EngineIntrinsics.SessionState.Path とたどると、PathIntrinsics クラスに行きつきます。この GetResolvedPSPathFromPSPath メソッドを使うと、Resolve-Path 相当のことができます。
存在しないパスなら例外を投げてくれるので、あとはそれを ValidationMetadataException に包んでやれば、自分で例外メッセージを考えなくてもいいというメリットはあります。
が、PathIntrinsics クラスのメンバーでは、パスがコンテナーを指すのか、リーフを指すのかを知る方法がありません。
実在確認とタイプの判別に別の方法を使っても構わないのですが、一貫した方法でチェックできた方がより良いだろうと考えました。
パスのタイプが希望するものでなかった場合にも例外を投げることを考えると、その場合の例外は自分で作らないといけないことになります。結局、自分でやらなければならないのなら、実在確認の方の例外も自分でやってしまおうと思いました。
そこで今回は、EngineIntrinsics.InvokeProvider.Item から ItemCmdletProviderIntrinsics を得て、これを使うことにしました。
こちらであれば、Exists メソッドで実在確認が、IsContainer メソッドでタイプの判別ができます。
そうそう。引数の型をコレクション型に変換するのに、今回は LanguagePrimitives.GetEnumerable メソッドを使ってみました。同クラスのメソッドには、他にも型変換に使えそうなものがいくつかありますので、興味があったらチェックしてみてください。
使い方
Add-Type -Path .\PSValidator.dll
のようにして読み込んでやれば使うことができます。
自作の属性は、Type Accelerators に登録されていないため、名前空間名をつける必要がある点に注意してください。
全コード
GitHub で公開しています。煮るなり焼くなり好きにしてください。