XDocument を用いて XML を処理する

スクリプトを書いていると、たびたび XML のファイルを作成したり、あるいは変更したりする機会があります。例えば、XML 形式の設定ファイルを自動生成したり、その設定ファイルを更新するようなケースです。

PowerShell で標準で用意されている XML のサポートは、DOM をベースにした XmlDocument クラスを用いるものです。当然ながら、この仕組みを用いてスクリプトを書くことに全然問題はありません。

しかし、シンプルかつよりスマートな System.XML.Linq が使える現在、PowerShell ベースとはいえ今更 DOM ベースの XmlDocument を使うのは、少し心理的に抵抗がある人もいるでしょう(例えば筆者)。XmlDocument と XDocument で、機能的に大きな違いはないのですが、XML 標準の DOM という考え方には幾分クセがあり、XmlDocument のその独特の使用方法に馴染むまで少しムズムズする可能性があります(ちなみに私は10年来ずっとムズムズしたままです)。

そういうわけで、XDocument を手軽に扱うための PowerShell モジュールを作ってみました(最後にソースを貼り付けてあります)、というのが今回のお題です。基本的にはコンストラクターをラップしたコマンドレットが中心で、以下のような形で使うことを想定しています。

$doc = New-XDocument ( `
  New-XElement users `
    (New-XElement user (New-XAttribute name ichiro)) `
    (New-XElement user (New-XAttribute name andrej)) `
)

これは新規に以下のような XML 文書のオブジェクトである XDocument を生成しています。

<?xml version='1.0'? encoding='utf-8'>
<users>
  <user name='ichiro' />
  <user name='andrej' />
</users>

さすがにこれだとちょっとタイプ量が半端ないので、alias も仕込んでみました。これで以下のようにスッキリと書けます。

$doc = xd (xe users (xe user (xa name ichiro)) (xe user (xa name andrej)))

ユーザーを追加してみましょう。XPath を使って users のノードを取り出し、そのノードに新しい user ノードを子供として追加します。

$users = $doc | Select-XNode users
$users.Add((xe user (xa name taro)))

Select-XNode は [System.Xml.Linq.Extensions]::XPathEvaluate のラッパーです。ただそのままラップすると芸が無いので、PowerShell っぽく引数をパイプラインから取れるように細工しています。

ファイルへの保存およびファイルからの読み出しも用意してあります。

$doc | Set-XDocument .\users.xml 
$doc = Get=XDocument .\users.xml

さて、生成したり、変更したり、といった観点で XDocument は非常に使い勝手がいいのですが、XmlDocument の使い勝手のほうが遥かによい場合があります。これは PowerShell 限定の話なのですが、PSObject として XmlDocument がラップされる際、属性や子供の要素の値を簡単にアクセスできるように、属性や子供の要素の名前のプロパティが自動で生成されます。例えば、さきほどのユーザーリストの XML 文書、ユーザーの名前を取り出したい場合、XmlDocument だと以下のように直感的にかけます。

[xml] $doc = Get-Content .\users.xml 
$names = @($doc.users.user | Foreach { $_.name })

最初、これと同じことを XDocument で出来るといいかなぁ、と思い、生成した XDocument / XElement に NoteProperty を後からくっつける……ようなことをちょっと考えていたんですが、ちょいと面倒、というか不毛な感じがしてきたのでやめました。

代わりに、もっと簡単な方法ですが、XmlDocument と XDocument を相互変換できるようにしてみました。利用シーンに応じて適切に使い分けるといいでしょう。

$doc = xd (xe users (xa version 1) (xe user (xa name ichiro)) (xe user (xa name andrej))) |
  ConvertFrom-XDocument

$names = $doc.users.user | Foreach {$_.name }

逆に、XmlDocument を取り扱っているときに、更新の場合だけ XDocument に変更するのもありです。

明日は Jolly.Jive さんの番です。お楽しみに!

 

モジュールのソースコード

 [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Xml")[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")function New-XAttribute (  [parameter(Mandatory=$true)]  [PSObject] $Name,  [parameter(Mandatory=$true)]  [string] $Value) {  New-Object System.Xml.Linq.XAttribute @($Name, $Value)}function New-XElement (  [parameter(Mandatory=$true,Position=0)]  [string] $Name,  [parameter(Mandatory=$false,ValueFromRemainingArguments=$true)]  [PSObject[]] $Children = @()) {  $xname = [System.Xml.Linq.XName]::Get($Name)  $element = New-Object System.Xml.Linq.XElement $xname  $Children | Foreach { $element.Add($_) }  $element}function New-XDocument (  [parameter(Mandatory=$false,ValueFromRemainingArguments=$true)]  [PSObject[]] $Children = @()) {  $doc = New-Object System.Xml.Linq.XDocument  $Children | Foreach { $doc.Add($_) }  $doc}function Select-XNode (  [parameter(Mandatory=$true)]  [string] $XPath,  [parameter(Mandatory=$true,ValueFromPipeline=$true)]  [System.Xml.Linq.XNode] $Node) {  process {    [System.Xml.XPath.Extensions]::XPathEvaluate($Node, $XPath)  }}function ConvertTo-XDocument (  [parameter(Mandatory=$true,ValueFromPipeline=$true)]  [xml] $Source) {  process {    $reader = New-Object System.Xml.XmlNodeReader $Source    try {      [void] $reader.MoveToContent()      [System.Xml.Linq.XDocument]::Load($reader)    } finally {      $reader.Close()    }  }}function ConvertFrom-XDocument (  [parameter(Mandatory=$true,ValueFromPipeline=$true)]  [System.Xml.Linq.XDocument] $Source) {  process {    $reader = $Source.CreateReader()    try {      $doc = New-Object System.Xml.XmlDocument      $doc.Load($reader)      $doc    } finally {      $reader.Close()    }  }}function Get-XDocument (  [parameter(Mandatory=$true)]  [string] $Path,  [parameter(Mandatory=$false)]  [System.Text.Encoding] $Encoding = [System.Text.Encoding]::UTF8) {  if (-not (Test-Path $Path)) {    throw "Unable to find '$Path'."  }  $Path = @(Get-ChildItem $Path)[0].FullName  $reader = New-Object System.IO.StreamReader @($Path, $Encoding)    try {    [System.Xml.Linq.XDocument]::Load($reader)  } finally {    $reader.Close()  }}function Set-XDocument (  [parameter(Mandatory=$true)]  [string] $Path,  [parameter(Mandatory=$true,ValueFromPipeline=$true)]  [System.Xml.Linq.XDocument] $Document) {  process {    $Document.Save($Path)  }}New-Alias xe New-XElementNew-Alias xa New-XAttributeNew-Alias xd New-XDocumentNew-Alias sx Select-XNodeExport-ModuleMember -Function '*'Export-ModuleMember -Alias '*'