What are some good ways to generate/store large XML blocks

by willsteele at 2012-08-15 19:17:18

In some of my scripts I have begun to use just plain text XML with the format specified to create objects. Here is an example of one such "template" I have take to generate XML. It is not that large, but, I think it demonstrates the challenge I face fairly well.

$template_xml_issue = @"
<Issue Archive="{0}" Locked="{1}">
<GUID>{2}</GUID>
<Author>{3}</Author>
<State>{4}</State>
<Description>{5}</Description>
<Comments>
<Comment Author="{6}" Created="{7}">{8}</Comment>
</Comments>
<Created>{9}</Created>
</Issue>
"@;

function New-Issue
{
param(
$Archive = $false,
$Locked = $false,
$Guid = [Guid]::NewGuid.Guid,
$Author = $env:USERNAME,
[ValidateSet(‘Closed’,‘Open’)]
$State = ‘Open’,
$Description,
$Comment_Author = $env:USERNAME,
$Comment_Created = (Get-Date),
$Comments,
$Created = (Get-Date)
)

$template_xml_issue -f $Archive,
$Locked,
$Guid,
$Author,
$State,
$Description,
$Comment_Author,
$Comment_Created,
$Comments,
$Created
}

$arguments = @{
Archive = $false;
Locked = $false;
State = ‘Open’;
Description = ‘Description’;
Comment_Author = $env:USERNAME;
Comments = ‘I got stuff to day.’;
};
$issue = Format-Xml (New-Issue @arguments)


It takes much less code and seems to work just as well, but, the down side is that it relies on here strings. What are some other ways I can store large XML chunks? I tried using PSD1 files with ConvertTo-Text, but, that seems to work for key/value, single-line data strings. I could generate the XML by building a node object, and, appending children across the board, but, I found this way to be easier to visualize.
by poshoholic at 2012-08-16 08:53:34
Here’s a technique I got found in a number of places (PoshCode, blogs) that I have adopted in my work and it works great.

You can create XML documents using Linq and a library that allows you to structure it in your code. For example, I have a file called NewXmlDocument.Library.ps1 that I use in a few of my modules. This library is dependent on Linq so the module manifest is configured with the following property to make sure that the required library is loaded when the module is used:

RequiredAssemblies = @(
‘System.Xml.Linq.dll’
)


Here are the contents of the NewXmlDocument.Library.ps1 file (using fully qualified names because I use this in a proxy function generator and I don’t want to have proxy functions generated that break the generator):

#region Xml Linq helper functions.

function New-XmlComment {
[CmdletBinding()]
[OutputType([System.Xml.Linq.XComment])]
param(
[Parameter(Position=0, Mandatory=$true)]
[System.Management.Automation.PSObject]
$Comment
)
try {
if ($Comment -notmatch ‘^ \S+ $’) {
$Comment = " $Comment "
}
Microsoft.PowerShell.Utility\New-Object -TypeName System.Xml.Linq.XComment -ArgumentList $Comment
} catch {
throw
}
}
Microsoft.PowerShell.Utility\Set-Alias -Name xc -Value New-XmlComment

function New-XmlElement {
[CmdletBinding()]
[OutputType([System.Xml.Linq.XElement])]
param(
[Parameter(Position=0, Mandatory=$true)]
[System.Xml.Linq.XName]
$Name,

[Parameter(Position=100, ValueFromRemainingArguments=$true)]
[Alias(‘args’)]
[System.Management.Automation.PSObject]
$ArgumentList
)
try {
Microsoft.PowerShell.Utility\New-Object -TypeName System.Xml.Linq.XElement -ArgumentList $(
$Name
while ($ArgumentList) {
$attrib, $value, $ArgumentList = $ArgumentList
if ($attrib -is [System.Management.Automation.ScriptBlock]) {
& $attrib
} elseif ($value -is [System.Management.Automation.ScriptBlock] -and
‘-Content’.StartsWith($attrib)) {
& $value
} elseif ( $value -is [System.Xml.Linq.XNamespace]) {
Microsoft.PowerShell.Utility\New-Object -TypeName System.Xml.Linq.XAttribute -ArgumentList ([System.Xml.Linq.XNamespace]::Xmlns + $attrib.TrimStart(‘-’)),$value
} else {
Microsoft.PowerShell.Utility\New-Object -TypeName System.Xml.Linq.XAttribute -ArgumentList $attrib.TrimStart(‘-’),$value
}
}
)
} catch {
throw
}
}
Microsoft.PowerShell.Utility\Set-Alias -Name xe -Value New-XmlElement

function New-XmlDocument {
[CmdletBinding()]
[OutputType([System.Xml.Linq.XDocument])]
param(
[Parameter(Position=0, Mandatory=$true)]
[System.Xml.Linq.XName]
$Root,

[Parameter()]
[ValidateNotNull()]
[System.Version]
$Version = [System.Version]‘1.0’,

[Parameter()]
[ValidateSet(‘Unicode’,‘UTF7’,‘UTF8’,‘UTF32’,‘ASCII’,‘BigEndianUnicode’)]
[System.String]
$Encoding = ‘UTF8’,

[Parameter()]
[Alias(‘NotStandalone’)]
[System.Management.Automation.SwitchParameter]
$RequireExternalEntityResolution,

[Parameter(Position=100, ValueFromRemainingArguments=$true)]
[Alias(‘args’)]
[System.Management.Automation.PSObject]
$ArgumentList
)
begin {
try {
if (($Root | Microsoft.PowerShell.Utility\Get-Member -Name NamespaceName -ErrorAction SilentlyContinue) -and
$Root.NamespaceName) {
function New-XmlDefaultElement {
param(
[System.Xml.Linq.XName]
$Tag
)
if ((-not ($Tag | Microsoft.PowerShell.Utility\Get-Member -Name NamespaceName -ErrorAction SilentlyContinue)) -or
-not $Tag.NamespaceName) {
$Tag = $Root.Namespace + $Tag
}
New-XmlElement -Tag $Tag @ArgumentList
}
Microsoft.PowerShell.Utility\Set-Alias -Name xe -Value New-XmlDefaultElement
}
} catch {
throw
}
}
process {
$standalone = ‘Yes’
if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey(‘RequireExternalEntityResolution’) -and $RequireExternalEntityResolution) {
$standalone = ‘No’
}
Microsoft.PowerShell.Utility\New-Object -TypeName System.Xml.Linq.XDocument -ArgumentList @(
Microsoft.PowerShell.Utility\New-Object -TypeName System.Xml.Linq.XDeclaration -ArgumentList $Version,$Encoding,$standalone
New-XmlElement -Name $Root @ArgumentList
)
}
end {
Microsoft.PowerShell.Utility\Set-Alias -Name xe -Value New-XmlElement
}
}
Microsoft.PowerShell.Utility\Set-Alias -Name xd -Value New-XmlDocument

#endregion


With that library loaded, you could then define your xml document using this like a small DSV within PowerShell, like this:

xd -Root Issues -Archive PoshoholicTestArchive -Locked $false {
xe GUID {[System.Guid]::NewGuid().ToString(‘d’)}
xe Author {‘Kirk Munro’}
xe State {‘State? Dude, I’‘m Canadian.’}
xe Description {‘Poshoholic at large, PowerShell MVP’}
xe Comments {
xe Comment -Author ‘Kirk "Poshoholic" Munro’ -Created $([System.DateTime]::Now.ToString()) {‘XML is pretty easy to create this way!’}
}
xe Created {[System.DateTime]::Now.ToString()}
}


I think that’s a really elegant way to do this without requiring here-strings. It even escapes your strings properly so that you end up with well formed XML in the end. Note that the parameter names are arbitrary. You can use whatever names you want and it just knows what to do. Is that cool or what? :slight_smile:
by poshoholic at 2012-08-28 18:59:33
I should also note that you can include loops inside of that DSV to create multiple entries in one level. I think this is better than here-strings because you can visualize the layout in your script.
by willsteele at 2012-08-28 19:12:48
Sorry for letting this one lie dormant. Been having some other issues suck up all my waking hours. Plus, having to reread some LINQ to get my head around it. This post alone, however, got me to start working on a LINQ chapter in my book. It also, indirectly spurned on that other thread. Let me revisit it and see what I am missing. Thanks.