Add-Member を極める

この記事は PowerShell Advent Calendar 2013 の 23 日目です。
昨日は @ichiohta さんの「New-Module を用いてカスタムオブジェクトを生成する」、明日は @oota_ken さんです。

いやー、一年ぶりでございます。
結局前回の記事は「前編」と銘打っておきながら、後編を書かずに終わってしまいました。
いつか整理してまとめたいものです。

さて、本日のお題は Add-Member について。
PowerShell 3.0 から、カスタムオブジェクトを組み立てるのが

[PSCustomObject] @{ x = 1; y = 2 }

といった簡易な記述で可能になったので、影が薄くなったコマンドです(昔はどう書いていたかは後述)。
かなりの長編となりますが、前後編に分けたりするとまた尻切れで終わる可能性がありますのでどうか最後までお付き合いください。

こいつの大きな謎は、MemberType パラメーターの多彩さです。
ヘルプによると、以下の値が指定できるようです(実は他にもあります)。

  • NoteProperty
  • AliasProperty
  • ScriptProperty
  • CodeProperty
  • ScriptMethod
  • CodeMethod

しかしながら、カスタムオブジェクトを組み立てるという用途では、使うのはせいぜい NoteProperty か AliasProperty くらいだったのではないでしょうか。
では、他のタイプはどのように利用するのか。
MemberType に上記の値を指定し、ValueSecondValue に適切な値を指定してやればよいのですが、その「適切な値」とは何なのか。
これが、ドキュメントもロクに無いのです。

また、このパラメーターの型は PSMemberTypes なのですが、定義されているものの中には、上記のリストにないものがあります(ParameterizedProperty とか)。
では、それらの中で MemberType パラメーターの値として使えるものと使えないものの違いは何なのか。

というわけで今回は、手探りで突き止めた MemberType 全解説ということをやってみたいと思います。

NoteProperty

まずは復習を兼ねて簡単なのから行きましょう。
NoteProperty は、オブジェクトに静的な値を持つメンバーを追加するために使います。

$object | Add-Member -MemberType NoteProperty -Name Hoge -Value 'Hello'

というコマンドを実行すると、$object に Hoge という名前で 'Hello' という値を持つプロパティを追加することができます。
この値は(C# の static とは違う意味で)静的です。つまり、明示的に再設定しない限り、この値が変化することは無いということです。

NoteProperty は最もよく使うためか、Add-Member コマンドにはこのためのパラメーターセットが 2 つも用意されています(PowerShell 3.0 から)。
まずは、プロパティを 1 つ追加する場合の書き方(パラメーターセット名:NotePropertySingleMemberSet)。

$object | Add-Member -NotePropertyName Hoge -NotePropertyValue 'Hello'

なんと、最初の例より 1 文字短縮できました!というのは冗談として。
パラメーター名を省略して書くと

$object | Add-Member Hoge 'Hello'

のように、グッと短く、わかりやすくなります。

もう一つは、複数のプロパティを一括して追加する場合(パラメーターセット名:NotePropertyMultiMemberSet)。
名前と値のペアをハッシュテーブルで指定します。

$object | Add-Member -NotePropertyMembers @{ x = 1; y = 2 }

また、冒頭で示した

[PSCustomObject] @{ x = 1; y = 2 }

というコードも、指定したプロパティは NoteProperty になります。
この書き方が使えなかった頃は、カスタムオブジェクトを作るのに Add-Member を使っていたのです(他にも幾つか方法はあります)。

AliasProperty

AliasProperty は、既存のプロパティに対する別名です。
実体となるプロパティ名を Value パラメーターで指定します。

$object | Add-Member -MemberType AliasProperty -Name Hoge2 -Value Hoge

これで、Hoge2 を Hoge の別名として使うことができます。
別名ですから、Hoge の値が変われば Hoge2 の値も変わります(逆も同様)。
また、既存の AliasProperty に対して、さらに別名をつけることも可能です。

$object | Add-Member -MemberType AliasProperty -Name Hoge3 -Value Hoge2

後述する ScriptProperty や CodeProperty に対する別名をつけることもできますし、.NET クラスのプロパティ(Add-Member では扱えない Property タイプ)も対象にすることができます。
なお、ParameterizedProperty は、プロパティと言いつつ実態はメソッドに近いもの(C# 風に言うとインデクサー)なので、AliasProperty で別名をつけることはできません。

また、AliasProperty は、SecondValue に型を指定することができます。
例えば、

$object | Add-Member -MemberType AliasProperty -Name HogeAsInt -Value Hoge -SecondValue [int]

のように書くと、Hoge プロパティの値を int 型にキャストして得ることができます。

ScriptProperty

ScriptProperty は、その実体がスクリプトブロックであるプロパティです。
C#Visual Basic をご存じならわかりやすいかと思いますが、これらの言語では、プロパティの実体は、値を取得するための Getter メソッドと、値を設定するための Setter メソッドのペアになっています(どちらか片方だけの場合もあります)。
PowerShell でも同じように、それぞれのメソッドをスクリプトブロックで書いてやることで、プロパティとして見せることができます。
Value に Getter、SecondValue に Setter を指定します。
プロパティの値を取得するときは Value に指定したスクリプトブロックが、値を設定するときは SecondValue に指定したスクリプトブロックが、それぞれ実行されます。
Setter の方には param キーワードでパラメーターを(1 つだけ)定義することができます(定義しない場合は $args[0] で取得できます)。
NoteProperty との大きな違いは、値が動的、つまり、明示的に再設定しなくても変化し得るということです。

$object | Add-Member -MemberType ScriptProperty -Name Now -Value { Get-Date }

とやれば、現在時刻を返すプロパティを作れますし、

$object | Add-Member -MemberType ScriptProperty -Name FullName `
-Value {
    '{0} {1}' -f $this.FirstName, $this.LastName
} `
-SecondValue {
    param($fullName)
    
    $name = $fullName -split ' '
    $this.FirstName = $name[0]
    $this.LastName = $name[1]
}

とやれば、実行時に値を加工することができます。
なお、後者の例では、あらかじめ $object に FirstName、LastName というプロパティを作っておく必要があります。
この場合、FullName プロパティを設定しなくても、FirstName もしくは LastName を書き換えれば、FullName の値も変わることになります。

ちなみに、C#Visual Basic では Setter のみのプロパティというものを作ることができます(.NET Framework のクラスライブラリにも少数ながら存在します)が、ScriptProperty の場合は Value パラメーターが必須なので、書き込み専用のプロパティを作ることはできません(上記の前者の例のように、Value のみ指定することで、読み取り専用のプロパティを作ることはできます)。

CodeProperty

CodeProperty は ScriptProperty と似ています。
違うのは、ScriptProperty が Getter と Setter をスクリプトブロックで記述していたのに対して、CodeProperty では、C# 等の .NET 言語でメソッドとして記述するということです。

イメージとしては、C#Visual Basic における拡張メソッドと同じです。
上記の ScriptProperty の例を C# で書いてみましょう。

using System;
using System.Management.Automation;

public static class PersonExtension
{
    public static string GetName(PSObject target)
    {
        string firstName = (string)target.Properties["FirstName"].Value;
        string lastName = (string)target.Properties["LastName"].Value;

        return string.Format("{0} {1}", firstName, lastName);
    }

    public static void SetName(PSObject target, string name)
    {
        string[] names = name.Split(' ');

        target.Properties["FirstName"].Value = names[0];
        target.Properties["LastName"].Value = names[1];
    }
}

気を付けておくべきは、

  • CodeProperty は .NET クラスのプロパティではなく、メソッドを対象にする。
  • メソッドの第一引数は PSObject 型で、ここには $this が渡される。
  • .NET コードから PowerShell オブジェクトのプロパティにアクセスするには Properties[名前].Value を使う。

といった点でしょうか。
このコードを Visual Studio を使ってコンパイルして、そのアセンブリを参照してもいいのですが、PowerShell で部分的に C# のコードを使いたい場合、Add-Type コマンドを使うのが簡単です(もちろん、この機能を試してみるのに簡単だという話であって、常に推奨されるわけではありません。実際には、よほど小規模でない限り、.NET 開発は Visual Studio でしっかりと行い、そのアセンブリPowerShell から参照するのがよいでしょう)。

$code = @'
// ここに上記の C# コードが入ります
'@

Add-Type -TypeDefinition $code

なお、Add-Type コマンドは PowerShell 使いであれば必修と言っていいコマンドです。
ここでは使い方を解説しませんが、ぜひ覚えておきましょう。

さて、ScriptProperty ではスクリプトブロックを使いましたが、CodeProperty では、Getter、Setter の各メソッドを MethodInfo オブジェクトで渡します。ScriptProperty 同様、Getter を Value に、Setter を SecondValue に指定します。
そこで、以下のようにして MethodInfo オブジェクトを取得します。

$getNameMethod = [PersonExtension].GetMethod('GetName')
$setNameMethod = [PersonExtension].GetMethod('SetName')

あとは簡単ですね。

$object | Add-Member -MemberType CodeProperty -Name FullName -Value $getNameMethod -SecondValue $setNameMethod

なお、この場合も Value のみ指定することで、読み取り専用にすることができます。

ScriptMethod

ここで折り返し。
プロパティ編は終わり、ここからはメソッド編です。
と言っても、プロパティ編がわかっていれば難しいことはありません。

というわけで、ScriptMethod
だいたいお分かりかと思いますが、実体がスクリプトブロックで表されるメソッドです。

$object | Add-Member -MemberType ScriptMethod -Name Greet -Value { 'Hello' }

$object.Greet() と呼び出すと、'Hello' という値を返します。
ScriptProperty と違って、引数は好きなだけ渡せます。ただし、[Parameter(Mandatory)] のような属性をつけても無視されてしまうようです。残念。

$object | Add-Member -MemberType ScriptMethod -Name Greet -Value {
  param($Greeting, $FirstName, $LastName)
  '{0}, {1} {2}' -f $Greeting, $FirstName, $LastName
}

$object.Greet(’Good morning', 'Power', 'Shell')

…特に言うことはありませんね。

CodeMethod

.NET クラスで定義された静的メソッドを MethodInfo として指定する以外は、ScriptMethod と同じです。

$code = @'
using System;
using System.Management.Automation;

public static class PersonExtension
{
    public static object Hello(PSObject target, string firstName, string lastName)
    {
        return target.Methods["Greet"].Invoke("Hello", firstName, lastName);
    }
}
'@

Add-Type -TypeDefinition $code

$helloMethod = [PersonExtension].GetMethod('Hello')
$object | Add-Member -MemberType CodeMethod -Name Hello -Value $helloMethod

$object.Hello('Power', 'Shell')

上で定義した Greet を C# 経由で呼び出してみました。
なお、ScriptMethod 同様、Hello メソッドのパラメーターに [Parameter(Mandatory = true)] などはつけることができませんでした(コンパイルエラーになります)。残念。

MemberType に使えるタイプ、使えないタイプ

メソッド編はさくっと終わりにしまして、第三部、In The Deep 編。
Add-Member のドキュメントで MemberType パラメーターに使えると書かれているタイプはやり尽くしてしまったわけですが、PSMemberTypes 型には他にも値が定義されています。
では、使える値と使えない値の違いは一体何なのか。

使えない値その 1:複数の値の組み合わせ

'0x{0:x4}' -f [int][System.Management.Automation.PSMemberTypes]::NoteProperty

のようにして生の値を見てみるとわかるかと思います(値は 16 進数で表記しています)。

名前
AliasProperty 0x0001
CodeProperty 0x0002
Property 0x0004
NoteProperty 0x0008
ScriptProperty 0x0010
Properties 0x001f
PropertySet 0x0020
Method 0x0040
CodeMethod 0x0080
ScriptMethod 0x0100
Methods 0x01c0
ParameterizedProperty 0x0200
MemberSet 0x0400
Event 0x0800
Dynamic 0x1000
All 0x1fff

名前が複数形になっていることからもわかりますが、PropertiesMethodsAll の 3 つは、単一のタイプではなく、複数のタイプの組み合わせです。
これらは Get-Member コマンドで、複数のタイプの一括指定を簡単にするために使うものであって、Add-Member で使うためのものではありません。

使えない値その 2:コンストラクターがない

この事実に気付いたことが、Add-Member への理解を深めるきっかけとなりました。
PowerShell の型システムについて体系立った資料をきちんと読めば書いてあることなのかもしれませんが、これに気付いたのは全くの偶然でした(だって資料は英語ですし…)。
とりあえず、PSNoteProperty クラスのドキュメントをさらっと見てみてください。

じつは、Add-Member で NoteProperty を追加するということは、内部的にはこの PSNoteProperty クラスのインスタンスを作って、それをオブジェクトに追加するということなのです(厳密に言うと違うと思いますが、細かいことは置いといてください)。
他にも各タイプについて、似たようなクラスが存在します。
AliasProperty であれば PSAliasProperty クラスが対応しますし、ParameterizedProperty であれば PSParameterizedProperty クラスが対応します。
これらはすべて、PSMemberInfo クラスの(間接的な)派生クラスになっています。

さて、では、Add-Member で使えるタイプ、例えば PSNoteProperty と、使えないタイプ、例えば PSParameterizedProperty の間には、何か違いがあるのでしょうか。
先ほど、Add-Member でメンバーを追加するということは、対応するクラスのインスタンスを作成して、オブジェクトに追加することだと言いました。
ということは、そのインスタンスを作成することができなければ、追加することもできません。
クラスのドキュメントを見てもらえれば気付くかもしれませんが、PSParameterizedProperty には(public な)コンストラクターがありません。そのため、インスタンスを作ることができないのです。
これは、他の使えないタイプ(Property、Method、Event、Dynamic)にも言えることです。

指定する値はコンストラクターを見ろ

そして、使えるタイプでは、ValueSecondValue に何を渡せばよいのかということも、ここから(なんとなく)わかります。
そう、各タイプに対応するクラスのコンストラクターの引数を見ればよいのです。
例えば、PSScriptProperty クラスのコンストラクター を見ると、名前の他には ScriptBlock を 1 つ取るものと 2 つ取るものがあります。前者が読み取り専用、後者が読み書き可能なプロパティに対応します。
あるいは、PSCodeMethod クラスのコンストラクター について見ますと、親切にも、codeReference 引数のところに

The referenced method must be a public static method where the first parameter is a PSObject object.

などと書かれています。
public で static で、第一引数に PSObject を取るメソッドですよ、と言うことですね。
まぁこれが、すべての使用可能なタイプについてちゃんと書かれていれば苦労は無いのですが、PSCodeProperty には何にも書いてありません。まったく。

この辺りについて深入りするには、PowerShell の Extended Type System (ETS) について読め、ということが MSDN に書いてあるのですが、英語だし面倒なので読みません。誰か翻訳して簡単に教えてください。お願いします。

Add-Member と types.ps1xml と Update-TypeData

PSMemberTypes 徹底解説にはまだ続きがあるのですが、ここから先の説明をするにあたり、ちょっと補足説明をしておきます。

Add-Member は types.ps1xml というファイル、および Update-TypeData コマンドと密接に関係しています。というか、だいたい同じことができます。
Add-Member は、実行時に動的にオブジェクトにメンバーを追加します。
一方、types.ps1xml と Update-TypeData は、オブジェクトの型を定義し、その型にメンバーを追加します。
個別のオブジェクトにメンバーを追加するときは Add-Member を使いますが、型を定義して、その型のオブジェクトすべてに同じメンバーを持たせたい場合は、 types.ps1xml に書いておくと自動的に追加してくれるので便利です。

既定の types.ps1xml ファイルは、PowerShell のインストール場所(組み込み変数 $PSHOME で示されます)にあります。
このファイルは変更できませんので、新しい型の定義は、別のファイルを作成して読み込む必要があります。モジュールが導入する型であれば、モジュール定義ファイル(*.psd1)に読み込むよう設定しておくことができますし(TypesToProcess)、ユーザーが独自に定義した型であれば、PowerShell プロファイルに Update-TypeData コマンドで読み込むように書いておくと、自動的に読み込んでくれます。
本記事では、そうした追加のファイルも含めた総称として types.ps1xml と呼ぶことにします。
詳細は about_Types.ps1xml や Update-TypeData コマンドのドキュメントを参照してください。

なお、モジュールに関しては、アドベントカレンダー 2 日目に @guitarrapc_tech さんがこちら、昨日は @ichiohta さんがこちらで解説してくださっています。

TypeName パラメーター

この節はおまけみたいなものなので、スキップしてもらっても構いません。

Add-Member には TypeName パラメーターがあります。
このパラメーターを指定すると、型に新しい名前を付けることができます。
一切メンバーを追加せず、名前を付けるだけということもできます(パラメーターセット名:TypeNameSet)。
Add-Member というコマンドを使っておきながら、メンバーを追加しないというのもどうかと思うのですが…

ただし、この名前は New-Object などでは使えません。
この名前は、先ほど説明した types.ps1xml や、オブジェクトの書式を定義する format.ps1xml で参照されます。
ちょっとマニアックな言い方をしますと、$object.PSTypeNames に新しい名前を追加することと同義です。

あらかじめ types.ps1xml なり Update-TypeData なりで型とメンバーを定義しておいてから、Add-Member コマンドでオブジェクトに型名を付けると、その時点から、そのオブジェクトにメンバーが増えます(もちろん、正常に機能するかどうかはオブジェクト次第です)。
乱暴な話をすれば、

$object | Add-Member -TypeName string

で、一見 string っぽく見える(string 型と同じメンバーを持つ)オブジェクトを作れます。当然ですが、中身は全然別物なので、使い物にはなりません。

MemberSet

さて、残されたタイプの解説に戻りましょう。
Add-Member のドキュメントには書いてない、しかし public なコンストラクターがあるという、ややこしいタイプがあるのです(types.ps1xml の解説には書いてあります)。
その一つが MemberSet (PSMemberSet) です。

MemberSet というのは要するに、オブジェクトの複数のメンバーをグループ化して名前を付けたものです。

Add-Member で MemberSet を追加するには、Value パラメーターに PSMemberInfo の配列を渡す必要があります(コンストラクターを参照)。PSMemberInfo というのは前述した、各タイプに対応するクラスの基底クラスです。
ですから、まず、グループ化したいメンバーの情報を、対応する各クラスを使って作成し、それらを配列にして Add-Member に渡せばよいということになります。

$noteProperty = New-Object System.Management.Automation.PSNoteProperty Member, 1
$properties = @( $noteProperty )

$object | Add-Member -MemberType MemberSet -Name Group -Value $properties

みたいなコードを書くと、

$object.Group.Member

で NoteProperty にアクセスできます。

隠し MemberSet について

MemberSet タイプは、ひょっとすると、PowerShell のヘビーユーザーであれば目にしたことがあるかもしれません。

$object | Get-Member -MemberType MemberSet -Force

とやると、$object が持つ MemberSet を見ることができます(Force がポイントです)。
PowerShell では、すべてのオブジェクトは、このようにいくつかの MemberSet を既定で持っています。

やや脱線になってしまいますが、簡単に解説しておきましょう。

名前 内容
PSBase PowerShell によって拡張されていない、元の .NET オブジェクトのメンバーを含む
PSAdapted TypeAdapter によって追加されたメンバーを含む(たぶん)
PSExtended Add-Member や types.ps1xml で追加したメンバーを含む
PSObject ベースオブジェクトを PSObject に変換するアダプター

PSAdapted は Get-Member のドキュメントでは「Windows PowerShell Extended Type System で定義されたメンバーを含む」とか書いてありますが、何のことやら。
PSObject の説明も何のことやらですが…。CodeProperty の節で見たように、PowerShell の型システムを通じてメンバーにアクセスするためのラッパーのようなものでしょうか。

ともあれ、PowerShell には型を拡張するために「TypeProvider」と「Add-Members 等」という 2 つの方法があるんだな、という感じに捉えておいてください。

例えば、こんなコードを書いてみます。

$xml = [xml] '<Message>Hello.</Message>'
$xml | Add-Member Note 1

$xml.GetType().FullName

$xml | Get-Member
$xml.PSBase
$xml.PSAdapted
$xml.PSExtended

PowerShellxml 型は .NET で言うと XmlDocument です。
Get-Member では、XmlDocument 型のメンバーに加えて、Message および Note プロパティがあるのがわかります。
PSBase には XmlDocument 型のメンバーしか含まれておらず、Message は PSAdapted に、Note は PSExtended に入っています。
ここでは、PowerShell の型システムについて、これ以上の深入りはしません。何となく、そんな感じだな、と思って頂ければ(謎)。

ちなみに、特別な名前を持つ MemberSet はもう一つあります。PSStandardMembers という名前で、すべてのオブジェクトが持っているわけではありません。
この MemberSet は、types.ps1xml ファイルで定義されています。探してみてください。
PSStandardMembers に属するプロパティの名前は、Update-TypeData のパラメーター名に対応します。
それぞれの意味は、Update-TypeData コマンドのドキュメントを参照してください。

MemberSet と AliasProperty

どうやら MemberSet 内のプロパティに対する AliasProperty は作成できないようなのです。
このようなコードで実験してみます。

$noteProperty = New-Object System.Management.Automation.PSNoteProperty Member, 'Inner'
$aliasProperty = New-Object System.Management.Automation.PSAliasProperty InnerAlias, Member

$properties = @( $noteProperty, $aliasProperty )
$object | Add-Member -MemberType MemberSet -Name Group -Value $properties

$object | Add-Member -MemberType NoteProperty -Name Member -Value 'Outer'
$object | Add-Member -MemberType AliasProperty -Name OuterAlias -Value Member

$object.Group.InnerAlias
$object.OuterAlias

MemberSet には、Member という名前で 'Inner' という値を持つ NoteProperty と、Member を参照する InnerAlias という AliasProperty があります。
一方、オブジェクト自体にも Member という名前の NoteProperty がありますが、こちらは 'Outer' という値を持ちます。
また、Member を参照する OuterAlias という AliasProperty があります。

このケースでは、InnerAlias と OuterAlias のいずれもが、'Outer' という値を指し示すことに注目してください。
つまり、InnerAlias は Group 内の Member ではなく、オブジェクト自体が持つ Group 外の Member を参照していることになります。
他にも、AliasProperty の参照先名を 'Group.Member' とするなど、何パターンか試してみたのですが、MemberSet 内のプロパティを示す AliasProperty は作れませんでした。

なお、ScriptProperty のスクリプトブロック中から MemberSet 内のメンバーにアクセスする場合は、普通に

$this.Group.Member

で可能です。
また、CodeProperty の場合は、

(string)((PSMemberSet)target.Members["Group"]).Properties["Member"]

のようにしてアクセスできます。
Group は Properties では取得できない点に注意してください。

PropertySet

Add-Member のドキュメントに書いてないのにコンストラクターがある奴、その 2。PropertySet (PSPropertySet)です。
そうは言っても、だいたい MemberSet と一緒だろうと思われるかもしれません。が、これがそうでもないんです。

PSPropertySet クラスのコンストラクターの第 2 引数に与えるのは、プロパティ名の配列であるようです。
そこで、こんなコードを試してみます。

$object | Add-Member -MemberType NoteProperty -Name X -Value 1
$object | Add-Member -MemberType NoteProperty -Name Y -Value 2
$object | Add-Member -MemberType NoteProperty -Name Z -Value 3

$object | Add-Member -MemberType PropertySet -Name Group -Value 'X','Y','Z'

一応、うまく行きます。
しかし、MemberSet のように、

$object.Group.X

とやっても、値を取得することはできません。

PropertySet は、オブジェクトが持つプロパティの一部をグループ化して表示する場合に使用します。
MemberSet は階層を作りますが、PropertySet はフラットなままです。
例えば、

$object | Select-Object -Property Group

とすることで、3 つのプロパティを一度に取得できます。
オブジェクトが非常に多数のプロパティを持っており、かつ、その一部を取得する処理を何度も繰り返すような場合には便利そうです。

実は PropertySet は、その存在をはっきりと意識しないだけで、PowerShell ユーザーであれば必ずお世話になっています。
たとえば、

$process = Get-Process powershell
$process | Format-List

とやって、プロセス情報を取得すると、既定では、Id、Handles、CPU、Name の 4 つのプロパティしか表示されません。
しかし、

$process | Format-List -Property *

とやると、このオブジェクトがそれよりはるかに多いプロパティを持っていることがわかります。

この 4 つのプロパティだけ表示するという設定は、types.ps1xml ファイルに書かれています。
MemberSet のところで PSStandardMembers について説明しましたが、その中に DefaultDisplayPropertySet という名前の PropertySet があります。
先ほどの 4 つのプロパティはここに書かれています(System.Diagnostics.Process で検索してみてください)。

<MemberSet>
  <Name>PSStandardMembers</Name>
  <Members>
    <PropertySet>
      <Name>DefaultDisplayPropertySet</Name>
      <ReferencedProperties>
        <Name>Id</Name>
        <Name>Handles</Name>
        <Name>CPU</Name>
        <Name>Name</Name>
      </ReferencedProperties>
    </PropertySet>
  </Members>
</MemberSet>

これは Update-TypeData コマンドでも設定できるのですが、その場合は、DefaultDisplayPropertySet パラメーターに各プロパティ名を列挙しなければなりません。
Update-TypeData コマンドに PropertySet 名を渡しても、その PropertySet 自体が表示されてしまいました。

ところで、types.ps1xml ファイルをよく見ますと、PSConfiguration とか PSResources、PSStatus といった、名前が PS で始まる PropertySet を持つオブジェクトがあります。
このあたりは、何か意味ありげなものを感じつつも、何なのかよくわかりません。

VariableProperty

PSNoteProperty クラスのドキュメントを見ていた時に、気付かれた方はいらっしゃるでしょうか?
このクラスの派生クラスに、PSVariableProperty というやつがいるのです。
とは言え、PSMemberTypes に VariableProperty などという値はありませんから、Add-Member で扱うことはできません。
PSVariableProperty クラスのインスタンスを生成してどうにかしようと思ったら、MemberSet に含めるしかありません。

コンストラクターの引数は 1 つしかなく、PSVariable 型。これは、Get-Variable コマンドの戻り値として得られます。
この型には、他の PSMemberInfo 派生クラスと異なる点があるのにお気づきでしょうか。
コンストラクターの引数にメンバー名を取らないのです。

そういうわけで、早速試してみます。

$Variable = 'Hello'
$VariableProperty = New-Object System.Management.Automation.PSVariableProperty (Get-Variable Variable)
$object | Add-Member -MemberType MemberSet -Name Group -Value @($VariableProperty)

という風に追加して、

$object.Group

を見てみますと、Variable というメンバーがいます。どうやら、変数名がそのままメンバー名になるようです。

$object.Group.Variable

で値にアクセスすることができます。

$Variable の値を変更すると、$object.Group.Variable の値も追従して変わります。逆も同様に、このメンバーの値を変えると、変数の値も変わります。
また、乱暴極まりないことですが、

Remove-Variable Variable

として変数を削除してしまうと、メンバーからも値を取得できなくなってしまいます。

AdaptedProperty

PSProperty クラスの派生クラスに PSAdaptedProperty というクラスがありまして、public なコンストラクターがあります。
そのため、このクラスはインスタンスを作成することができます(PSProperty クラスには public なコンストラクターがありません)。
しかし、PSAdaptedProperty のインスタンスを作って MemberSet に追加しようとしても、

Add-Member : PSProperty オブジェクトまたは PSMethod オブジェクトは、このコレクションに追加できません。

というエラーになってしまいます。

PSAdaptedProperty は、MemberSet の節で簡単に説明した TypeAdapter(PSPropertyAdapter)によって追加されたプロパティを表すものです。
これは、Add-Member とは異なる機構によって追加されるプロパティですので、Add-Member では扱うことができません。

まとめ

……まとまらねぇー。
ええと、誰か PowerShell の型システムについて詳しく教えてください。以上。
っていうか MSDN の ETS (Extended Type System) に関するリンクが

This Topic Is No Longer Available

ってどういうことですかマイクロソフトさん!