Creating More Custom XML with PowerShell

PowerShell-Text-Purple-hero
I hope you’ve been following along with my series on working with XML in PowerShell. If you are just jumping in, you definitely want to get caught up first or material in this article might not make sense. In the previous article, I demonstrated how you might use ConvertTo-XML to create a custom XML file. In this article, I want to go through an alternate process.
 

 
Before we get started, let me re-iterate the importance of planning what you intend to do with the final XML file. If your intent is to serialize data to bring back into PowerShell, then Export-Clixml is the best tool to use. And even if you need to use ConvertTo-XML, if you can start with a complete object, that will simplify the process. But let’s look at a situation in which you need to assemble an XML file from a variety of parts.
As I did in the previous article, I’m going to use a list of computer names.

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

With this list, I’m going to retrieve some system information from different WMI classes using Get-CimInstance. I’ll convert each result to an XML stream, which, if you recall, will be an array of XML strings.

$os = Get-CimInstance Win32_Operatingsystem -ComputerName $computers |
Sort PSComputername |
Select @{Name="Computername";Expression={$_.PSComputername}},InstallDate,
Caption,Version,OSArchitecture | ConvertTo-XML -as Stream
$cs = Get-Ciminstance Win32_Computersystem -ComputerName $computers |
Sort PSComputername |
Select PSComputername,TotalPhysicalMemory,HyperVisorPresent,NumberOfProcessors,
NumberofLogicalProcessors | ConvertTo-XML -as Stream
$services = Get-Ciminstance Win32_Service -ComputerName $computers |
Sort PSComputername |
Select PSComputername,Name,Displayname,StartMode,State,StartName |
ConvertTo-XML -as stream

I’m sorting the results by the computer name to make it easier to align all of the results. There’s no limit to the amount of information you might need, but I’ll keep things relatively simple. My goal is to create a single XML file that might look like the following:

<?xml version="1.0" encoding="utf-8"?>
<Computers>
    <Computer>
        <OperatingSystem>...</OperatingSystem>
        <ComputerSystem>...</ComputerSystem>
        <Services>
            <Service>...</Service>
        </Services>
    </Computer>
</Computers>

Now that I know where I want to go, I can begin assembling the final file from the pieces at hand. All I’m really doing is parsing and joining text, so I’ll define a here string to begin with. This way I can add the formatted and final XML text as I process my pieces.

$myXML = @"
<?xml version="1.0" encoding="utf-8"?>
<Computers>
"@

Next, I need to process each collection of WMI data. I’m going to trust that none of the servers I queried went offline in the middle of collecting data. The operating system and computer system results should be the same since there will be one object for each computer.

Comparing array sizes
Comparing array sizes (Image Credit: Jeff Hicks)

I can also verify the XML contents.
Viewing computer details
Viewing computer details (Image Credit: Jeff Hicks)

As I did in the last article, I need to either rename “<Object>” or take matters into my own hands. Let’s do that.
I intend to write my own XML content to the pipeline and eventually send it all to a file.  This will require some parsing and splitting of the XML content I’ve saved. Because the computer and operating system entries are the same, I can use a For loop to create the necessary beginning tags. This means I don’t need the <Object> tags. All I want is each <Property> element.

for ($i = 2 ; $i -lt ($computers.count+2);$i++) {
 "<Computer>"
 "  <OperatingSystem>"
 #split the string into an array and skip the first and last lines
 $prop = $os[$i].split("`n") | Select -Skip 1 | Select -SkipLast 1
 #split the properties into separate strings and insert as indented strings
 $prop.split("`n") | foreach {
 "     $_ "
 }
 "  </OperatingSystem>"
 "  <ComputerSystem>"
 $prop= $cs[$i].split("`n") | Select -Skip 1 | Select -SkipLast 1
 #filter out PSComputername as it would be redundant
 $prop.split("`n") | Where {$_ -notmatch "PSComputername"} | foreach {
 "     $_ "
 }
 "  </ComputerSystem>"
 "</Computer>"
}

I’m creating a series of for strings, which includes indentation to make the result more legible. The output is formatted XML.

Custom XML
Custom XML (Image Credit: Jeff Hicks)

The last part is the collection of services for each computer. This is a little trickier, as I need to select the corresponding services for each computer. In my case, this means managing 1,375 services over 10 different servers. In fact, trying to parse the text I have from my original command that created $services will be extremely convoluted.

Instead, I’ll create a hashtable using Group-Object, where the key will be each computername.

$svcHash = Get-Ciminstance Win32_Service -ComputerName $computers |
Select PSComputername,Name,Displayname,StartMode,State,StartName |
Group-Object -Property PSComputername -AsHashTable -AsString

With this hashtable, I can create an XML stream for a given computer omitting everything except the object details.

$svcHash.'chi-fp02' | Select * -ExcludeProperty PSComputername |
ConvertTo-XML -as stream | Select -skip 2 | Select -SkipLast 1

Although, as I did above, all I want are the Properties, so I’ll need to do the same type of splitting and filtering.

Splitting and Filtering Text
Splitting and Filtering Text (Image Credit: Jeff Hicks)

Let’s plug this into the For loop for each computer. I’ll need to pull the computername from the earlier data.

#get the computername from the OS property
 [xml]$x = $os[$i].split("`n").where({$_ -match "computername"})
 $Computername = $x.Property.'#text'

With this, I can get the corresponding entry from hashtable, convert the properties to XML and parse as I did earlier.

"  <Services>"
 $objs = $svcHash.$computername | Select * -ExcludeProperty PSComputername |
 ConvertTo-XML -as stream | Select -skip 2 | Select -SkipLast 1
 foreach ($item in $objs) {
 "     <Service>"
 $item.split("`n").Where({$_ -match '<Property'}).split("`n") | foreach {
 "        $_"
 }
 "     </Service>"
 }
 "  </Services>"

Once I know I’m getting the output I expect, I can modify my For loop to add to the here string.

for ($i = 2 ; $i -lt ($computers.count+2);$i++) {
 $myXML+= "<Computer>`n"
 $myXML+= "  <OperatingSystem>`n"
 #split the string into an array and skip the first and last lines
 $prop = $os[$i].split("`n") | Select -Skip 1 | Select -SkipLast 1
 #split the properties into separate strings and insert as indented strings
 $prop.split("`n") | foreach {
 $myXML+= "     $_ `n"
 }
 $myXML+= "  </OperatingSystem>`n"
 $myXML+= "  <ComputerSystem>`n"
 $prop= $cs[$i].split("`n") | Select -Skip 1 | Select -SkipLast 1
 #filter out PSComputername as it would be redundant
 $prop.split("`n") | Where {$_ -notmatch "PSComputername"} | foreach {
 $myXML+= "     $_ `n"
 }
 $myXML+= "  </ComputerSystem>`n"
 #get the computername from the OS property
 [xml]$x = $os[$i].split("`n").where({$_ -match "computername"})
 $Computername = $x.Property.'#text'
 $myXML+= "  <Services>`n"
 $objs = $svcHash.$computername | Select * -ExcludeProperty PSComputername |
 ConvertTo-XML -as stream | Select -skip 2 | Select -SkipLast 1
 foreach ($item in $objs) {
 $myXML+= "     <Service>`n"
 $item.split("`n").Where({$_ -match '<Property'}).split("`n") | foreach {
 $myXML+= "        $_ `n"
 }
 $myXML+= "     </Service>`n"
 }
 $myXML+= "  </Services>`n"
 $myXML+= "</Computer>`n"
}

I needed to add the new line marker (`n) to each line. The last step is to close the XML and save the results to a file.

$myXML+= "</Computers>"
$myXML | Out-File -filepath c:\work\custom1.xml -Encoding utf8

But now I have the XML file I’m expecting.

Complete custom XML
Complete custom XML (Image Credit: Jeff Hicks)


If that seemed like a lot of work, well it was. Your head might even be spinning a bit. Sorry about that. If you truly need a custom XML file, it might be better to generate it completely from scratch using the .NET Framework. But I’ll save that for next time.