Creating Custom XML in PowerShell

PowerShell-Text-Purple-hero
I hope you’ve been following along in our exploration of working with XML in PowerShell. If you are just jumping in, I encourage you to take a few minutes to read the previous articles in this series. In the last article, I demonstrated a number of ways to get PowerShell output into an XML file. But sometimes, you have to take matters into your own hands to create the exact XML format that you need. Before you begin, I want to re-iterate the importance of planning ahead. How will you be using the XML files? Will you be re-using them in a PowerShell session? Will they be processed by some external XML-driven application or process? Will humans need to interact with them or machines? I’m going to demonstrate several techniques but understand that there is no single best practice.
 

 
In the previous article, I introduced you to ConvertTo-Xml. Because the cmdlet doesn’t immediately create a file, you have the option of modifying the XML first. Here’s one scenario.
I have a list of computers.

#process list of computers filtering out those offline
$computers = Get-Content S:\myservers.txt | Where { Test-WSMan $_ -ErrorAction SilentlyContinue}

I want to get disk information from each computer and save the results to an XML file. This XML file will be consumed by an external trend reporting process. In addition to the information I get back from Get-CimInstance, I need to include at least one custom property and I want to change a few other things in the file. Here’s my code to create an XML stream and save it to a variable.

#get disk data and convert to an XML stream
$xmlStream = Get-Ciminstance Win32_logicaldisk -filter "drivetype=3" -ComputerName $computers |
select * -ExcludeProperty CimClass,Cim*Properties |
Add-Member -MemberType ScriptProperty -Name "PctFree" -Value {[math]::round(($this.freespace/$this.size)*100,4)} -PassThru |
ConvertTo-XML -as Stream

I am excluding any of the CIM system properties and using Add-Member to create a custom property. The XML stream will include my custom property.

Converted XML
Converted XML (Image Credit: Jeff Hicks)

I know that eventually, I will want to change ‘PSComputername’ and instead of Object and Objects, use more meaningful node names.
The variable, $xmlStream is actually an array of different XML elements. Item 0 is the XML declaration.
The XML directive
The XML directive (Image Credit: Jeff Hicks)

Technically, the declaration should include an encoding directive, so I’ll simply assign a new value.

$xmlStream[0] = '<?xml version="1.0" encoding="utf-8"?>'

Next, instead of ‘Objects’ and ‘Object’, I want the outer node to be ‘Disks’ and ‘Disk’. The outer tags are simple enough to change.

$xmlStream[1] = "<Disks>"
$xmlStream[-1] = "</Disks>"

Don’t forget that XML is case-sensitive, so your tags must match. Next, I need to modify each of the nodes, replacing key pieces of text. I’ll use a FOR loop to go through each disk element.

for ($i = 2;$i -lt $($xmlStream.count -1);$i++) {
 $xmlStream[$i] = $xmlStream[$i] -replace "PSComputername","Computername"
 $xmlStream[$i] = $xmlStream[$i] -replace "(?<=\<(\/?))Object","Disk"
}

The last replacement is done with a fancy regular expression pattern utilizing a lookback, so I can change both <Object> and </Object> with a single line of code. But now my array of XML elements is properly formatted. All that remains is to send the results to a file.

$xmlStream | Out-File -FilePath c:\work\Disks.xml -Encoding utf8

Note that I encoded the file to match the XML declaration. To simplify the process I would wrap all of this up in a PowerShell function or a script.
Another approach might be to convert everything to a single string.

$xmlString = Get-Ciminstance Win32_logicaldisk -filter "drivetype=3" -ComputerName $computers |
select * -ExcludeProperty CimClass,Cim*Properties |
Add-Member -MemberType ScriptProperty -Name "PctFree" -Value {[math]::round(($this.freespace/$this.size)*100,4)} -PassThru |
ConvertTo-XML -as String

I can easily make the same kind of adjustments.

$xmlString = $xmlString -replace 'xml version="1.0"', 'xml version="1.0" encoding="utf-8"'
$xmlString = $xmlString -replace "PSComputername","Computername"
$xmlString = $xmlString -replace "(?<=\<(\/?))Object","Disk"
$xmlString | Out-File -FilePath c:\work\Disks-string.xml -encoding utf8

There are additional ways to create custom XML from scratch, but I think this may be enough for now. There’s plenty here for you to experiment with and try on your own. And don’t worry, I’ll address how to bring these custom files back into PowerShell in a future article. As always, comments and questions are welcome.