はじめに
本記事は PowerShell Advent Calendar 2018 の 4 日目です。
PowerShell Advent Calendar 2018 は寄稿して頂ける方を絶賛募集中です。よろしくお願いいたします。
qiita.com
TL; DR
- ps1xml を書こうぜ。
- RELAX NG はいいぞ。
ps1xml とは
ps1xml とは、PowerShell と共に利用する、特殊な XML ファイルです。
あからじめ PowerShell に同梱されているものもありますし、ユーザーが作成したモジュールを配布する際に同梱することもできます。
PowerShell に同梱されているものは、Windows PowerShell のインストール ディレクトリ*1にあります。
ただし、このファイルは過去との互換性のためにあるもので、Windows PowerShell 5.1 では読み込まれないのだそうです。PowerShell Core でも同様です。
ユーザーが自作したものは読み込まれますので、サンプルとしては有用です。
blog.shibata.tech
blog.shibata.tech
ps1xml には、types.ps1xml と format.ps1xml という2つのタイプがあります。
モジュール作りの一環としてこれらを書くにあたって、エラー チェックや入力補完をしてくれる XML スキーマが無いと厳しいので書いたよ、というのが、本記事の趣旨です。
書いたスキーマはここに置いてあります。
types.ps1xml
PowerShell は .NET のオブジェクト モデル上に構築されており、パイプラインを通じて、コマンド間を .NET オブジェクトが流れるという点が、文字列をやり取りする Linux 等のシェルと比較した際の最大の特徴であるということは、今更言うまでもありません。
PowerShell は型に緩い言語ではありますが、.NET オブジェクトである以上、何らかの型はあります。
PowerShell が採用している型システムは、.NET の共通型システム(CTS:Common Type System)を拡張した、拡張型システム(ETS:Extended Type System)というものです。
types.ps1xml は、ETS 上での型を定義するためのファイルです。
型を定義するということは、大雑把に言うと、型名と、その型に属するメンバーを定義するということです。
まったくゼロから独自の型を定義することもできますし、.NET (CTS) の型にメンバーを追加することもできます。
PowerShell のインストール ディレクトリには、以下の 3 つのファイルがあります。
- types.ps1xml
- typesv3.ps1xml
- getevent.types.ps1xml
Microsoft の公式資料としてはこのへんになるのでしょうか。
ただ、どうも詳細なリファレンスはリンク切れになっているようです。
以下の記事でも触れていますので、参考にしてみてください。
tech.blog.aerie.jp
format.ps1xml
format.ps1xml は、PowerShell コンソール上におけるオブジェクトの表示方法を定義するファイルです。
PowerShell でオブジェクトを整形して表示するコマンドとして、以下の 4 つがあります。
Format-Table と Format-List はよく使いますし、明示的に使わないとしても、暗黙のうちに使われることがありますから、目にする機会が多いと思います。
Format-Wide はマイナーで、あまり使ったことがありません。Format-Custom に至っては、一度も使ったことがありません…。
まぁともかく、PowerShell には、この 4 種類のフォーマットがあるということです。
format.ps1xml は、オブジェクトの型と、これらのフォーマットの組み合わせに対して、表示方法を定義します。
PowerShell のインストール ディレクトリには、以下のようなファイルがあります。
- DotNetTypes.format.ps1xml
- FileSystem.format.ps1xml
- Registry.format.ps1xml
- etc ...
Microsoft の公式資料としては、このへん。
以下の記事でも簡単に触れています。
tech.blog.aerie.jp
こちらも参考にどうぞ。
blog.shibata.tech
XML スキーマ言語
XML のスキーマを定義する言語はいくつかあります。
メジャーなところだと以下の 2 つ。
W3C XML Schema
XML スキーマといえばこれ。名前もそのまんまです。
XML それ自体の仕様を定義している W3C が策定したスキーマ言語です。
スキーマ ファイルの代表的な拡張子は *.xsd。
Visual Studio でも編集や検証に標準で対応していますし、.NET で使う XML ファイルも、大抵のスキーマは Visual Studio に同梱されています。
また、PowerShell コマンドのヘルプを書く場合に PSMaml という XML ベースの言語を使うことがありますが、このスキーマも XML Schema で書かれています。
興味がある方はこの辺をご覧ください。
tech.blog.aerie.jp
RELAX NG
W3C XML Schema の対抗仕様として策定された、もうひとつのスキーマ言語です。
XML Schema は W3C で仕様化されていますが、RELAX NG は OASIS で標準化され、ISO 規格にもなっています。
Visual Studio でサポートしていないのをはじめ、使える環境が少ないのが悩ましい点です。
スキーマファイルの典型的な拡張子は *.rng のようです。
公式サイトはこちら。
今回は RELAX NG
冒頭にも掲載しましたが、リポジトリはこちらにあります。
github.com
PowerShellTypeDefinitions.rng が types.ps1xml、PowerShellFormatDefinitions.rng が format.ps1xml のスキーマになっています。
拡張子は *.rng ですね。はい、RELAX NG です。
今回、初めて書いてみたんですが、書きやすいです。XML Schema には戻りたくないくらい。
とはいえ、さすがにメモ帳で書くのは無理なので、Oxygen XML Editor というツールを買いました。
RELAX NG の特に気に入った点は、「順不同」が書きやすいということです。
たとえば、このように書くと、foo 要素の後に bar 要素が来なければならないという意味になりますが…
<element name="foo"> <text/> </element> <element name="bar"> <text/> </element>
これを <interleave> で囲むだけで、foo と bar のどちらが先に来てもよいという意味になります。
<interleave> <element name="foo"> <text/> </element> <element name="bar"> <text/> </element> </interleave>
さらに、foo と「bar または baz のどちらか」が順不同で出現するのは、こう書けます。とても直感的です。
<interleave> <element name="foo"> <text/> </element> <choice> <element name="bar"> <text/> </element> <element name="baz"> <text/> </element> </choice> </interleave>
XML Schema だとこうはいきません。
一応、<all> という要素を使うことで、順不同は表現できるのですが、all はとても制約が強く、使いづらい要素なのです。
たとえば 3 つ目の例は、all では書けません(all の中に choice を置くことができません)。つまり、XML Schema では書けません。詰みます。
そして、この「順不同」という性質は、特にプログラミング用途では重要です。
なぜなら、オブジェクトのメンバーには順序が無いからです。
ps1xml も、読み込まれた後は PowerShell 内部では .NET のオブジェクトになります。
types.ps1xml の内容は TypeData オブジェクトになりますし、format.ps1xml の内容は PSControl の派生オブジェクトになります。
XML を HTML のような文書記述言語だと考えるなら、要素の順番は重要です。
しかし、最終的にデシリアライズされてオブジェクトになるのなら、順不同でもよいでしょう。オブジェクトのメンバーをどれから初期化しようが、最終的に同じ状態になればよいからです。
そして、XML というのは往々にして、HTML のような人が読むための文書記述よりも、コンピューターが処理するための構造化されたデータの記述に使われることが多い言語です。
今回スキーマを書くにあたって参考にした PowerShell のソースコードでも、順不同に読み込めるようになっています。
自分で書く場合でも、経験上、順不同に書ける方が、格段に書きやすいです。
というわけで、特にオブジェクトにデシリアライズされることを意図した XML に対しては、RELAX NG の方が適していると思います。
このスキーマを試してみたいという方には、環境を選ぶ結果になってしまって申し訳ありません。
Schematron
PowerShellFormatDefinitions.sch というファイルがあります。
この拡張子 *.sch というのは、Schematron という言語で書かれたスキーマ ファイルです。
RELAX NG と Schematron は相互補完的なもので、ともに文書スキーマ定義言語(DSDL:Document Schema Definition Language)の一部分です。
types.ps1xml の方は、PowerShellTypeDefinitions.rng に Schematron が埋め込まれています。
なお、Schematron と XML Schema を組み合わせて利用することも可能です。
スキーマ ファイルの使い方
ps1xml ファイルに、これらのスキーマ ファイルを関連付けてエラー チェックをするには、xml-model 処理命令を使います。
こんな感じです。
<?xml version="1.0" encoding="utf-8"?> <?xml-model href="PowerShellFormatDefinitions.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> <?xml-model href="PowerShellFormatDefinitions.sch" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> <Configuration> ... </Configuration>
PowerShellTypeDefinitions.rng のように、Schematron を含んだ RELAX NG の場合、以下のように、同一のファイルを schematypens を変えて 2 回リンクする必要があるようです。
<?xml version="1.0" encoding="utf-8"?> <?xml-model href="PowerShellTypeDefinitions.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> <?xml-model href="PowerShellTypeDefinitions.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> <Types> ... </Types>
このへんは、使用するチェック ツールによっても異なってくるかもしれません。
上記の GitHub リポジトリを直接参照して頂いても構いませんが、これらのファイルが消えない保証は致しかねますので、その点だけご留意ください。
<?xml version="1.0" encoding="utf-8"?> <?xml-model href="https://raw.githubusercontent.com/aetos382/Aerie.PowerShell.Schemas/f195731306fbc932d42940d031b2eba0e65138eb/PowerShellFormatDefinitions.rng" type="application/xml" schematypens="http://relaxng.org/ns/structure/1.0"?> <?xml-model href="https://raw.githubusercontent.com/aetos382/Aerie.PowerShell.Schemas/f195731306fbc932d42940d031b2eba0e65138eb/PowerShellFormatDefinitions.rng" type="application/xml" schematypens="http://purl.oclc.org/dsdl/schematron"?> <Types> ... </Types>
型に別名をつける方法
types.ps1xml にせよ、format.ps1xml にせよ、オブジェクトの型名をキーにして機能します。
ただし、いずれも object.GetType() メソッドによって取得できる具象型名が対象であり、インターフェイスの型名などを書いても機能しません。
かといって、全部の具象型名を書きたくはないということもあるでしょう。*2
そういう場合は、型に別名をつけて、その名前を対象にしてやることができます。
たとえば、C# でこんな風に書いているコードがあったとします。
protected override void ProcessRecord() { ... this.WriteObject(foo); }
ここで、foo の具体的な型名を ps1xml に書きたくないので、抽象的な別名をつけたいとしましょう。
こうやります。
protected override void ProcessRecord() { ... var output = PSObject.AsPSObject(foo); output.TypeNames.Insert(0, "TypeNameAlias"); this.WriteObject(output); }
出力するオブジェクトを PSObject でラップしてやることで、別名をつけることができるようになります。
スクリプトでやる場合は、Add-Member コマンドを使います。
$foo | Add-Member -TypeName TypeNameAlias
こうすることで、.NET の型としては区別できない PSCustomObject であっても、用途に応じて任意の型名をつけることができます。
この名前に対して ps1xml を書いてやることができます。
... <Type> <Name>TypeNameAlias</Name> <Members> ... </Members> </Type> ...
モジュールの中で独自に定義したものでない型に対して ps1xml を書くこともできますが、そうすると、そのモジュールを読み込んでいるか否かで、他のオブジェクトの表示方法が変わってしまうことになります(そういう使い方を意図しているのであればアリだと思いますが)。
コマンドの出力として他者が定義した型を返す場合でも、別名をつけてやることで、そのコマンドから出力された場合にのみ、表示方法を変えることが可能になります。
型階層と ps1xml
PowerShell では、すべてのオブジェクトが、PSTypeNames という隠しプロパティを持っています(上記の C# のコードで別名をつける際に使用した TypeNames プロパティと同じものです)。
PSTypeNames は、デフォルトでは .NET の型階層を反映し、具体的な方から順に入っています(インターフェイスの型名は入りません)。
例えば以下のような型定義の場合
class Base { } class Derived : Base { }
こうなります
PS> $derived = New-Object -TypeName Derived PS> $derived.PSTypeNames Derived Base System.Object
Add-Member の場合、先頭に追加されます。
PS> $derived | Add-Member -TypeName TypeNameAlias PS> $derived.PSTypeNames TypeNameAlias Derived Base System.Object
型に ps1xml の内容を適用するにあたっては、これらの型名を先頭から順に見て行くのですが
- types.ps1xml の場合は、型階層のすべての型名が処理対象となる
- 基底型と派生型に同名のメンバーが定義されている場合は、より具体的な(PSTypeNames 内で先頭に近い)型に対して定義されたものの方が優先される
- もともとの .NET 型と types.ps1xml に同名のメンバーが定義されている場合は、ps1xml の方が優先される
- format.ps1xml の場合は、最初にマッチした型名が処理対象となる
という仕様のようです。
TypeNames についても、以下の記事でちょっと触れています。
tech.blog.aerie.jp
また、コマンドのパラメーターの型を PSTypeNames で制約することもできます。
tech.blog.aerie.jp
ps1xml ファイルの内容を適用する方法
ps1xml ファイルをモジュールに同梱して配布する場合は、モジュールのマニフェスト ファイル(*.psd1)に書いてやります。
こんな感じ。
# MyModule.psd1 ... TypesToProcess = @( 'MyModule.types.ps1xml' ) FormatsToProcess = @( 'MyModule.format.ps1xml' ) ...
詳細は New-ModuleManifest コマンドを参照してください。
アドホックに適用する場合は、
- types.ps1xml は Update-TypeData コマンド
- format.ps1xml は Update-FormatData コマンド
を使用します。
モジュールに同梱せず、他者が定義した型に対して、自分好みのメンバー追加やフォーマット定義をしたい場合は、PowerShell プロファイル内でこれらのコマンドを呼び出せばよいでしょう。
おことわり
このスキーマは PowerShell のパーサーのソースコードを見て書き起こしたものです。
一応、Windows PowerShell に同梱されている ps1xml ファイルに対しては、ほぼ 100% パスするように作ってあります。*3
ただ、完全なものであるという保証は致しかねます。
一応、参考にしたソースコードへのリンクを貼っておきます。
- types.ps1xml は TypeTable.cs
- format.ps1xml は typeDataXmlLoader.cs 他(partial クラスになっており、他にも同じフォルダー内にいくつかのソース ファイルがあります)。
このスキーマが PowerShell モジュール制作の一助になれば幸いです。
*1:通常は C:\Windows\System32\WindowsPowerShell\v1.0
*2:具象型がすごく多いとか、private な型だとか…
*3:「ほぼ」なのは、PowerShell のパーサーは要素名の大文字小文字も不問とする仕様なのですが、そこまで追従するのは面倒くさすぎたためです。