鷲ノ巣

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

パラメーターの検証属性について/前編

PowerShell では、関数のパラメーターに特定の属性をつけることで、内容に宣言的に制限をかけることができます。
宣言的にというのが重要で、つまり、パラメーターの内容をチェックするためのコードを書かなくてもよいということです。
本記事では、そうした属性についてまとめます。

前置き

以下の文中で「参照型」と言う場合、.NET Framework における一般的な意味での参照型を指し、

  • 配列型
  • 型指定していない場合(object 型)

を含みますが、string 型を含まないものとします。

.NET の常識からすると意外なことですが、PowerShell の string 型は $null を保持することができません。
$null を代入すると空文字列に変換されます。

PS > [string] $str = $null

PS > $str.Length
0

PS > $str | gm

   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone(), System.Object ICloneable.Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB), int IComparab...
Contains         Method                bool Contains(string value)
CopyTo           Method                void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int co...
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari...
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string...
...

実は、NullString.Value というのを代入してやれば $null になるのですが、まぁそれはこの際置いておきましょう。

また、string 型は、PowerShell でも

PS > 'ABC'[1]
B

のようにして、各文字へのインデックスアクセスが可能ですが、そのままでは char 型の配列としては扱われませんので、以下の文中に置いて「配列型」に string 型は含まれません。

[Parameter(Mandatory)] 属性

function Sample
{
    param(
        [Parameter(Mandatory)]
        [string] $x)
}

まず基本はこれです。
Parameter 属性の Mandatory パラメーターをセットすると、そのパラメーターを必須として宣言することができます。

Mandatory 指定したパラメーターはどうなるかと言うと…

  • パラメーターを指定せずにコマンドを呼び出そうとすると、その場で入力を促されます(デフォルト値は無視されます)。
  • パラメーターの型が参照型の場合、$null を指定することが禁止されます。
  • パラメーターの型が文字列型の場合、空文字列を指定することが禁止されます。
  • パラメーターの型が配列型の場合、空の配列を指定することが禁止されます。
  • パラメーターの型が参照型の配列の場合、配列の要素に $null を含むことが禁止されます。
  • パラメーターの型が文字列型の配列の場合、配列の要素に空文字列を含むことが禁止されます。

パラメーターの型が指定されていない(または明示的に object 型として宣言されている)場合には、$null は禁止されますが、空文字列、空の配列、$null を含む配列は渡すことはできます。
object 型の配列に対しては、$null、空の配列、$null を含む配列は禁止されますが、空文字列を含む配列は渡すことができます。

このようなややこしいルールを覚えるよりは、「Mandatory 指定は型指定しないと思うように作用しないので、必ず型指定するべし」と覚えておいた方が良いでしょう。
というか、スクリプトを書く際は、object 型であっても必ず明示し、型の省略はしない方が良いでしょう。

なお「空の配列を含む配列」は禁止されません。

Allow 系属性

Mandatory 指定は、かなり広範にわたる制限がかかります。
そのため、Mandatory 指定の制限を部分的に緩める属性が用意されています。
それが Allow 系属性群です。
Mandatory 指定していないパラメーターに対しては効果がありません。

[AllowNull()] 属性

function Sample
{
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        [object] $x)
}

Mandatory 指定したパラメーターに対して、

  • パラメーターの型が参照型の場合、$null を指定すること
  • パラメーターの型が参照型の配列の場合、その要素に $null を含めること

が許容されます。
繰り返しになりますが、PowerShell では文字列型は $null を保持できないので、文字列型のパラメーターに対してこの属性は効果がありません。

[AllowEmptyString()] 属性

function Sample
{
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string] $x)
}

Mandatory 指定したパラメーターに対して、

  • パラメーターの型が文字列型の場合、空文字列を指定すること
  • パラメーターの型が文字列型の配列の場合、その要素に空文字列を含めること

が許容されます。

[AllowEmptyCollection()] 属性

function Sample
{
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [string[]] $x)
}

Mandatory 指定したパラメーターに対して、空のコレクションを指定することを許容します。

Validate 系属性

ここからは、Mandatory 指定とは関係なく使用できる属性です。
Mandatory 指定していない(つまり、呼び出し時に省略可能な)パラメータですが、使用する場合はこの制限を満たさなければならないというものです。

また、変数にも付与することができます。例えば、

PS > [ValidateNotNullOrEmpty()] [string] $x = ''

というのは禁止されます。

属性を追加できません。値 の変数 x が無効になります。

Mandatory 指定では、パラメーターの型が正しくないと期待したような効果を発揮しませんでしたが、Validate 系属性はパラメーターの型に関係なく、パラメーターの値のみを見て動作します(だからといって型を指定しなくてもよいというわけでは、もちろんありません)。

なお、Validate 系属性と、前述の Allow 系属性の両方を指定すると(あまり意味はありませんが…)、Validate 系属性が優先されます。

[ValidateNotNull()] 属性

function Sample
{
    param(
        [ValidateNotNull()]
        [object] $x)
}
  • パラメーターの値の型が参照型の場合、$null を指定することを禁止します。
  • パラメーターの値の型が参照型の配列の場合、配列の要素に $null を含むことが禁止されます。

[ValidateNotNullOrEmpty()] 属性

function Sample
{
    param(
        [ValidateNotNullOrEmpty()]
        [string[]] $x)
}
  • パラメーターの値の型が参照型の場合、$null を指定することが禁止されます。
  • パラメーターの値の型が文字列型の場合、空文字列を指定することが禁止されます。
  • パラメーターの値の型が配列型の場合、空の配列を指定することが禁止されます。
  • パラメーターの値の型が参照型の配列の場合、配列の要素に $null を含むことが禁止されます。
  • パラメーターの値の型が文字列型の配列の場合、配列の要素に空文字列を含むことが禁止されます。

Mandatory 同様、空の配列を含む配列は禁止されません。

[ValidateCount()] 属性

パラメーターの値の型が配列型の場合、その要素数の最小と最大を指定します。
配列型でない場合は指定できません。

以下の例では、$x は要素数が 2 以上 4 以下の配列でなければなりません。

function Sample
{
    param(
        [ValidateCount(2, 4)]
        [string[]] $x)
}

上限または下限のみを指定する方法は提供されていませんので、適当な上下限値を指定する必要があります。
例えば、下限のみ指定したい場合は、以下のようにすることが考えられるでしょう。

function Sample
{
    param(
        [ValidateCount(1, [int]::MaxValue)]
        [string[]] $x)
}

なお、この属性は、入れ子になっている内部の配列の要素数には影響しません。

[ValidateLength()] 属性

パラメーターの値の型が文字列型、または文字列型の配列の場合、その最小または最大の長さを指定します。
それ以外の型に対しては使用できません。

以下の例では、$x は長さが 1 文字以上 8 文字以下の文字列でなければなりません。

function Sample
{
    param(
        [ValidateLength(1, 8)]
        [string] $x)
}

以下の例では、$x の要素は、長さが 0 文字以上 4 文字以下の文字列でなければなりません。

function Sample
{
    param(
        [ValidateLength(0, 4)]
        [string[]] $x)
}

なお、ここで言う文字数とは、string 型の Length プロパティが返すもののことを言います。
そのため、Unicodeサロゲートペアや合成文字の場合、見た目の文字数とは一致しない点には注意が必要です。

[ValidatRange()] 属性

パラメーターの値の上下限を指定します。
数値型だけでなく、文字列にも使うことができます。
配列型の場合は、個々の要素についてチェックします。

以下の例では、$x は 10 以上 20 以下でなければなりません。

function Sample
{
    param(
        [ValidateRange(10, 20)]
        [int] $x)
}

以下の例では、$x の要素は、'ABC' より大きく 'DEF' より小さい文字列でなければなりません。

function Sample
{
    param(
        [ValidateRange('ABC', 'DEF')]
        [string[]] $x)
}

ただ、文字列の大小比較のルールはよくわかりません…。
例えば、上記の関数には 'abc' を渡すことができません。

引数 abc が、最小許容範囲 ABC を下回っています。

と言われてしまいます。
文字コード順の比較で言えば、'ABC' が 0x41, 0x42, 0x43 なのに対して、'abc' は 0x61, 0x62, 0x63 なので、普通に考えると 'ABC' < 'abc' になると思うのですが、PowerShell においては 'ABC' -cgt 'abc' が $true になるので、'ABC' > 'abc' であるようです。どういうルールなのでしょうか…?

[ValidateSet()] 属性

パラメーターの値を、あらかじめ決められた一連の値のうちの一つに制限します。
数値型にも文字列型にも使用可能です。
配列型の場合は、個々の要素についてチェックします。

以下の例では、$x は 'Red', 'Green', 'Blue' のいずれかでなければなりません。

function Sample
{
    param(
        [ValidateSet('Red', 'Green', 'Blue')]
        [string] $x)
}

[ValidatePattern()] 属性

パラメーターの値を、指定された正規表現にマッチするものに制限します。
配列型の場合は、個々の要素についてチェックします。

正規表現パターンについては MSDN ライブラリを参照してください。
マッチは引数の一部でもいいため、全体をマッチさせたい場合はパターンの前後に ^ と $ を書くのを忘れないように気を付けましょう。

以下の例では、$x は、英数字で 3 文字連続する同じ文字を含んでいなければなければなりません。

function Sample
{
    param(
        [ValidatePattern('([A-za-z0-9])\1\1')]
        [string] $x)
}

[ValidateScript()] 属性

パラメーターの値は、指定したスクリプトブロックが $true を返すような値でなければなりません。
スクリプトブロック中では、パラメーターの値は $_ で参照できます。
パラメーターが配列型の場合、その要素が 1 つずつ渡されます。

以下の例では、$x は偶数の配列でなければなりません。

function Sample
{
    param(
        [ValidateScript({ $_ % 2 -eq 0 })]
        [int[]] $x)
}

残念ながら、スクリプトブロック型の変数は使用できません。

$sb = { [int] $_ % 2 -eq 0 }

function Sample
{
  param(
    [ValidateScript($sb)]
    [int[]] $x)
}

パラメーター属性は、定数またはスクリプト ブロックである必要があります。

あまり複雑なスクリプトを指定すると読みづらくなりますので、簡単な制約に留めるのが良いでしょう。

ValidateScript は非常に柔軟な検証が可能で、ValidateLength、ValidateRange、ValidateSet、ValidatePattern などをすべて ValidateScript で賄うことも可能です。
しかし、可読性の面からも、パフォーマンスの面からもお勧めできませんので、適切な属性を使うべきでしょう。

[PSTypeName()] 属性

パラメーターの値が、指定した型を持たなければならないという制約を課します。

これは少し詳しく説明しましょう。
より厳密に言うならば、パラメーターの値の PSTypeNames プロパティの中に、属性で指定した型名が含まれなければいけないということです。
まずは PSTypeNames について見てみましょう。

PS > $p = Get-WmiObject Win32_Process -Filter "ProcessID=$PID"
PS > $p | gm

   TypeName: System.Management.ManagementObject#root\cimv2\Win32_Process

Name                       MemberType     Definition                                                               
----                       ----------     ----------                                                               
Handles                    AliasProperty  Handles = Handlecount                                                    
ProcessName                AliasProperty  ProcessName = Name                                                       
PSComputerName             AliasProperty  PSComputerName = __SERVER                                                
VM                         AliasProperty  VM = VirtualSize                                                         
WS                         AliasProperty  WS = WorkingSetSize                                                      
...

PS > $p.PSTypeNames
System.Management.ManagementObject#root\cimv2\Win32_Process
System.Management.ManagementObject#root\cimv2\CIM_Process
System.Management.ManagementObject#root\cimv2\CIM_LogicalElement
System.Management.ManagementObject#root\cimv2\CIM_ManagedSystemElement
System.Management.ManagementObject#Win32_Process
System.Management.ManagementObject#CIM_Process
System.Management.ManagementObject#CIM_LogicalElement
System.Management.ManagementObject#CIM_ManagedSystemElement
System.Management.ManagementObject
System.Management.ManagementBaseObject
System.ComponentModel.Component
System.MarshalByRefObject
System.Object

PS > $c = Get-WmiObject Win32_ComputerSystem
PS > $c | gm

   TypeName: System.Management.ManagementObject#root\cimv2\Win32_ComputerSystem

Name                        MemberType    Definition                                                                                                                                                                                    
----                        ----------    ----------                                                                                                                                                                                    
PSComputerName              AliasProperty PSComputerName = __SERVER                                                                                                                                                                     
JoinDomainOrWorkgroup       Method        System.Management.ManagementBaseObject JoinDomainOrWorkgroup(System.String Name, System.String Password, System.String UserName, System.String AccountOU, System.UInt32 FJoinOptions)         
Rename                      Method        System.Management.ManagementBaseObject Rename(System.String Name, System.String Password, System.String UserName)                                                                             
SetPowerState               Method        System.Management.ManagementBaseObject SetPowerState(System.UInt16 PowerState, System.String Time)                                                                                            
UnjoinDomainOrWorkgroup     Method        System.Management.ManagementBaseObject UnjoinDomainOrWorkgroup(System.String Password, System.String UserName, System.UInt32 FUnjoinOptions)                                                  
...

PS > $c.PSTypeNames
System.Management.ManagementObject#root\cimv2\Win32_ComputerSystem
System.Management.ManagementObject#root\cimv2\CIM_UnitaryComputerSystem
System.Management.ManagementObject#root\cimv2\CIM_ComputerSystem
System.Management.ManagementObject#root\cimv2\CIM_System
System.Management.ManagementObject#root\cimv2\CIM_LogicalElement
System.Management.ManagementObject#root\cimv2\CIM_ManagedSystemElement
System.Management.ManagementObject#Win32_ComputerSystem
System.Management.ManagementObject#CIM_UnitaryComputerSystem
System.Management.ManagementObject#CIM_ComputerSystem
System.Management.ManagementObject#CIM_System
System.Management.ManagementObject#CIM_LogicalElement
System.Management.ManagementObject#CIM_ManagedSystemElement
System.Management.ManagementObject
System.Management.ManagementBaseObject
System.ComponentModel.Component
System.MarshalByRefObject
System.Object

WMI オブジェクトについて Get-Member してみると、TypeName の System.Management.ManagementObject の後に # に続いて WMI のクラス名が書かれているのがわかります。
また、PSTypeNames プロパティを参照すると、System.Management.ManagementObject の上に WMI の型階層があるのがわかりますね。

.NET Framework 的には、WMI オブジェクトはすべて ManagementObject クラスで表現されますが、これだけですと、例えば Win32_Process 型の値が欲しいコマンドに、Win32_ComputerSystem 型の値を渡すこともできてしまいます。
このように、.NET 的には区別できない型についても型制約を課すのが、PSTypeName 属性です。

この # 以降の型名は .NET の型システム上には無い情報ですので、Type クラスで表現できません。
そのため、以下のように書くことはできません(# のところで構文エラーになります)。

function Sample
{
  param(
    [System.Management.ManagementObject#root\cimv2\Win32_Process[]] $x)
}

代わりに、このように記述します。

function Sample
{
  param(
    [PSTypeName('System.Management.ManagementObject#root\cimv2\Win32_Process')]
    [object[]] $x)
}

この関数に対して、例えば Win32_ComputerSystem 型の値を渡そうとすると、

Sample : 引数をパラメーター 'x' にバインドできません。引数の PSTypeNames が、パラメーターで必要な PSTypeName と一致しません: System.Management.ManagementObject#root\cimv2\Win32_Process。

というエラーになります。

WMI と相性のいい仕組みですが、WMI だけのためにあるものではありません。
例えば、XML などで活用することも可能でしょう。
スキーマが異なる XML は、同じ xml 型(XmlDocument クラス)で扱っていても、別の型として扱ったほうがいい場合があります。

function Import-XmlTypeA
{
  [xml] (Get-Content .\typeA.xml) | Add-Member -TypeName System.Xml.XmlDocument#typeA -PassThru
}

function Import-XmlTypeB
{
  [xml] (Get-Content .\typeB.xml) | Add-Member -TypeName System.Xml.XmlDocument#typeB -PassThru
}

Add-Member については前回の記事を参照してください。

なお、この属性は変数につけることはできません(つけてもエラーにはなりませんが機能しません)。

次回予告

この後、「後編」と「おまけ編」の 3 部構成になる予定です。
後編では、Validate 系属性を自作してみます。
おまけ編は、「細かすぎて伝わらない系の話」になる予定です。