2015/12/21
[v5]パラメータ検証属性をclass構文で作ってみる
この記事はPowerShell Advent Calendar 2015の21日目の記事です。
PowerShellの属性
PowerShellには2.0から言語機能として「属性」機能が追加されました。PowerShellの属性はC#の属性とほぼ同じものですが、2.0~4.0の時点では、高度な関数(Advanced Function)を作成するために関数に付与するCmdletBinding属性(記述上ではparamブロックに付与する形になる)、関数のパラメータに付与するParameter属性やAlias属性等、関数のパラメータや変数に付与する各種検証属性(Validate~、Allow~系)くらいしか使うことはなかったかと思います。
このうちパラメータ検証属性については、あえとす氏が以前詳しく解説しておられます。:パラメーターの検証属性について/前編 - 鷲ノ巣
PowerShellで属性クラスを作る
さて話は変わって、つい先日、WMF 5.0 / PowerShell 5.0がRTMしました。Windows 7/8.1、Windows Server 2008R2/2012/2012R2用のインストーラーが公開されたので、もう使っておられる方もいると思います。ぎたぱそ氏が一晩で解説記事を書いてくれてますね。
PowerShell 5.0では言語機能としてclass構文がついに追加されました。これでついにPowerShell言語も真の意味でオブジェクト指向言語の要素を完備したと言えるかと思います。
このclass構文、.NET Frameworkの既存のクラスを基底クラスとして継承するなんてことも、普通にできてしまいます。そこで私が考えたのは、もしかしてこれで属性も作れるんじゃない?ということでした。
v5のclass構文の基本的な部分の解説は、また別の機会にということにしまして、今回はいきなり、属性を作る話をしていきます。
ちなみにC#の属性については、属性 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C が参考になります。PowerShellで属性を作る時も基本はだいたい同じかと思います。
属性クラスの例
属性パラメータ(コンストラクタ引数)にDescription、名前付きパラメータとしてNoを指定でき、classに適用可能なTest属性はこんな感じです。
[AttributeUsage([AttributeTargets]::Class)] class TestAttribute : Attribute { [string]$Description [int]$No TestAttribute($Description) { $this.Description = $Description } }
これを利用する際は、まずこのclassを含むスクリプトをドットソースで実行することで、グローバルに読みこむ必要があるようです(同一スクリプト内に属性定義と属性利用をまとめて書くとうまく動作しない)。
呼び出し側では以下のように指定します。
[TestAttribute("クラスの説明", No = 1)] class Foo { $X = 1 }
通常なら[Test()]のように"Attribute"の部分は省略できるはずなんですが、これも何故かフルネームで指定しないとダメなようでした。
クラスに属性が正しく指定されているかどうかはリフレクションで調べます。
[Attribute]::GetCustomAttributes([Foo])
結果は
Description No TypeId ----------- -- ------ クラスの説明 1 TestAttribute
こんな感じです。
パラメータ検証属性を作る
これだけでは面白くないので、少し実用になるかもしれない属性を書いてみましょう。
ところで前述のあえとす氏の記事の後編では、C#でPowerShellのパラメータ検証属性を作る方法について解説されています。ただ、C#でPowerShellの実行空間にアクセスして、操作を行ったり情報を取得したりするのは、少し知識が必要な部分かと思います。
そこで、PowerShellで使う属性なんだからPowerShellで書いたら楽になるんじゃない?という発想で、パラメータ検証属性をクラス構文で書いてみました。
このサンプルは、パラメータ(または変数)値の型が、指定の型セットに含まれているかどうかを検証するものです。複数型を取るようにするためにパラメータの型を[psobject]にすることが良くありますが、この属性を利用して、指定可能な型を絞り込めるようになります。
using namespace System.Management.Automation [AttributeUsage(([AttributeTargets]::Property) -bor ([AttributeTargets]::Field))] class ValidateTypeSetAttribute : ValidateEnumeratedArgumentsAttribute { [Type[]]$Types ValidateTypeSetAttribute($Types) { $this.Types = $Types } [void]ValidateElement([object]$element) { if(!($element.GetType() -in $this.Types)) { throw New-Object ValidationMetadataException ` "$($element.GetType().FullName) は許容されない型です。値は $($this.Types) のいずれかの型である必要があります。" } } }
使い方は以下の通り。パラメータ検証属性の場合は、定義と同一スクリプト内で呼び出しても、"Attribute"を省略しても問題ないようです。
function test { param( [ValidateTypeSet(,([int],[string],[double]))] [psobject] $obj ) } test "a" #OK test (Get-Process)[0] #NG
class構文のコンストラクタで可変長引数を定義する方法が分からなかったので、呼び出しがちょっと不格好ですが、そこはご容赦を。
プライバシーポリシー