XML

Practical Use: Manipulating XML Files With PowerShell

In my previous post I talked about getting values from XML files.  Today we’re going to update a value in an XML file.

As before, I’m not a PowerShell or XML guru, but what I’m going to cover below works for me and I can’t think of a reason it wouldn’t work for you.

We’re going to continue using the sample XML file provided by Microsoft.  Copy the XML content & paste into a new file, books.xml, & save it some place convenient.

Getting the Current Value

We want to change an author’s name from Stefan to Stephane.  First, let’s find the entries that we need to change:

[xml]$XML = Get-Content "C:\Users\Julius\Downloads\books.xml"
$XML.catalog.book | ? { $_.author -like '*stefan' }

XML-SetValue-001

Great only one result so we don’t need to loop!

Setting the New Value

We know the property we want is author so we can simply  do something like this to set the new value:


($XML.catalog.book | ? { $_.author -like "*stefan" }).author = "Knorr, Stephane"

But I find it helpful to do something like this instead

$Node = $XML.catalog.book | ? { $_.author -like "*stefan" }

$Node = $XML.catalog.book | ? { $_.author -like "*stefan" }

"Old Author: {0}" -f $Node.author

$Node.author = $Node.author.Replace('Stefan','Stephane')

"New Author: {0}" -f $Node.author

XML-SetValue-002.PNG

If you had multiple entries, as is the case with author Eva Corets, you could do this:

foreach($Result in ($XML.catalog.book | ? { $_.author -like "Corets*" }))
    {
        write-host "Book #$($Result.id) Written by $($Result.author)"
        $Result.author = 'Mendez, Eva'
        write-host "Book #$(Result.id) Updated to $($Result.author)`r`n
    }

XML-SetValue-003.PNG

Saving the Updated XML

The save operation is super easy:

$XML.Save("C:\Users\Julius\Downloads\book.xml")

Done!
But I’m a big fan of having backup copies, so I opt for something like:


[string]$XMLFile = "C:\Users\Julius\Downloads\books.xml"
[xml]$XML = Get-Content $XMLFile
foreach($Result in ($XML.catalog.book | ? { $_.author -like "Corets*" }))
    {
        "Book #{0} Written by {1}" -f $Result.id,$Result.author
        $Result.author = 'Mendez, Eva'
        "Book #{0} Updated to {1}`r`n" -f $Result.id,$Result.author
    }
Copy-Item -Path $XMLFile -Destination "$XMLFile.ORIG.$(Get-date -Format 'yyyymmdd_hhmmss')"
$XML.Save($XMLFile)

 

Function Set-XMLValue

Try to think of this function a more of a framework than a complete solution.  I recently had to update a few different XML files, some of which were on a few hundred machines and used this to do the heavy lifting.

Function Set-XMLValue
    {
        Param
            (
                [Parameter(Mandatory=$true)]
                    [string]$XMLFile,

                [Parameter(Mandatory=$true)]
                    [string]$XMLTreePath,

                [Parameter(Mandatory=$true)]
                    [string]$Property,

                [Parameter(Mandatory=$false)]
                    $OldValue,

                [Parameter(Mandatory=$true)]
                    $NewValue,

                [Parameter(Mandatory=$true)]
                    [string]$NewFile = $null
            )

        if(!(Test-Path -Path $XMLFile -PathType Leaf)) { return 2 }

        [bool]$DoUpdate = $false

        Try
            {
                [xml]$XML = Get-Content $XMLFile

                $XMLPath = '$XML.' + $XMLTreePath

                Foreach($Node in (Invoke-Expression $XMLPath | ? { $_.$Property -ieq "$OldValue" }))
                    {
                        # Check to confirm that particular property exists
                        if([string]::IsNullOrEmpty($Node)) { Write-host "ERROR: NO PROPERTY [$Property] FOUND CONTAINING ORIGINAL VALUE [$OldValue]"; [int]$Return = 2 }

                        # Get current value from XML
                        $CurrValue = $Node.$Property

                        # Phase 1: Analysis of parameters and values

                        # Check if the old value was specified
                        if($OldValue)
                            {
                                # When the old value is specified, the script check if the current value matches the old value.

                                # If the current value matches old value and will need to be updated
                                if($CurrValue -eq $OldValue) { $DoUpdate = $true }

                                # If the current value doesn't match the old value but matches the new, its already up to date
                                Elseif($CurrValue -eq $NewValue) { [int]$Return = 0 }

                                # If the current value doesn't match the old or new value we won't change anything but return the current value
                                Else { [string]$Return = "WARNING: The current value [$CurrValue] did not match the specified [$OldValue] so NO changes were made." }

                            }
                        # If an old value was not specified, we'll update regardless of the current value
                        Else
                            {
                                # If the current value doesn't match the new value it will need to be updated
                                if($CurrValue -ne $NewValue) { $DoUpdate = $true }

                                # If the current value matches the new value its already up to date
                                elseif($CurrValue -eq $NewValue) { [int]$Return = 0 }
                            }

                        # Phase 2: Performing the update if deemed necessary
                        If($DoUpdate -eq $true)
                            {
                                # Update value
                                $Node.$Property = [string]$NewValue

                                # If we're using a new file, we don't need to backup the original file.
                                if([string]::IsNullOrEmpty($NewFile)) { $XML.Save($NewFile) }
                                Else
                                    {
                                        # Backup existing XML
                                        Copy-Item -Path $XMLFile -Destination "$XMLFile.ORIG.$(Get-date -Format 'yyyymmdd_hhmmss')" -Force -ErrorAction Stop

                                        # Save new/updated XML (overwrite existing)
                                        $XML.Save($XMLFile)
                                    }

                                # Success!
                                [int]$Return = 0
                            }
                    }
            }
        Catch { Write-Warning "ERROR DOING XML UPDATE OPERATION for [$XMLFile]: $_"; [int]$Return = 1 }
        return $Return
    }

$UpdateResult = Update-XMLValue -XMLFile "C:\Users\Julius\Downloads\books.xml" -XMLTreePath catalog.book -Property author -OldValue 'Knorr, Stefan' -NewValue 'Knorr, Stephane' -NewFile "C:\Users\Julius\Downloads\newbooks.xml"

write-host "UpdateResult [$UpdateResult]"

This may not be fancy, and arguably complicated, but if you’re dealing with multiple XML files, PowerShell can be a huge timesaver.

 

Good Providence!

Advertisements

Practical Use: Getting Values from XML Files

A number of applications we use in the organization have XML-based configuration files and occasionally the need arises to validate the settings from the configuration.  For example, we might want to verify that a user is pointed to the right server, or that a particular setting is set to X.  We could treat XML files the same way we would a typical text file, as seen here, but where’s the fun in that?

I’m not a PowerShell or XML guru, but what I’m going to cover below works for me and I can’t think of a reason it wouldn’t work for you.

To keep things simple, we’ll be working with a sample XML file provided by Microsoft.  Copy the XML content & paste into a new file, books.xml, & save it some place convenient.

Ingest An XML File

In order to get started, you need to pull in the XML into a variable for further manipulation as such:

[xml]$XML = Get-Content "C:\Users\Julius\Downloads\books.xml"
# OR
$XML = [xml](Get-Content "C:\Users\Julius\Downloads\books.xml")

Six of one, half a dozen of another; whatever works for you.

If you type $XML you should see something to the effect of:

XML-GetValues-001

Browsing the Structure

Once your XML variable is populated, it’s time to walk the XML tree structure to find the key that contains the data you’re looking for; And in order to do that you have to know the tree structure.

Without going too deep into this

  • The first line is the XML prolog containing the version information
  • The second is the <catalog> and that is our root element or node
  • All of the <book> nodes are children of the root
  • Each <book> node has a number of children: author, title, genre, price, publish_date & description.

Keep in mind this is a pretty simple & basic XML example, so the structure doesn’t go too deep.  You may find that your application XML’s are several layers deep.

If I wanted to see all the books, I would use

$XML.catalog.book

Which would then list the books:

XML-GetValues-002.PNG

 

Locating the Right Data

If I wanted to find all books by written by authors Stefan Knorr and Eva Corets, I would use

$XMl.catalog.book | ? { $_.author -like &quot;*stefan&quot; }

$XMl.catalog.book | ? { $_.author -like &quot;Corets*&quot; }

That would quickly narrow the scope:

XML-GetValues-003.PNG

Getting Specific Values

To grab key bits of details from Stefan Knorr’s book I could do

$XML.catalog.book | ? { $_.author -like &quot;*stefan&quot;} | select id,price,publish_date

# OR

$Node = $XML.catalog.book | ? { $_.author -like &quot;*stefan&quot; }

$Node.id

$Node.price

$Node.publish_date

XML-GetValues-004.PNG

I try to be as specific as possible when searching for data.

 

Function Get-XMLValue

This function is more of a framework as it depends squarely on the XML you’re working with and what you want to retrieve.  I recently had to verify a setting on a few hundred machines, so I used a modified version of this function to do the heavy lifting.

The function below is geared to return the book description when supplied the ID.

Function Get-XMLValue
    {
        Param
            (
                [Parameter(Mandatory=$true)]
                    [string]$XMLFile,

                [Parameter(Mandatory=$true)]
                    [string]$XMLTreePath,

                [Parameter(Mandatory=$true)]
                    [string]$AnchorProperty,

                [Parameter(Mandatory=$true)]
                    [string]$AnchorValue,

                [Parameter(Mandatory=$true)]
                    [string]$Property
            )

        if(!(Test-Path -Path $XMLFile -PathType Leaf)) { return 2 }

        Try
            {
                [xml]$XML = Get-Content $XMLFile

                $XMLPath = '$XML.' + $XMLTreePath
                $Return = @()
                Foreach($Node in (Invoke-Expression $XMLPath | ? { $_.$AnchorProperty -ieq &quot;$AnchorValue&quot; })) { $Return += $Node.$Property }

                if(-not $Return.count -gt 0) { Write-host &quot;ERROR: NO PROPERTY [$Property] FOUND BASED ON QUERY: [$AnchorProperty] = [$AnchorValue]&quot;; [int]$Return = 2 }
            }
        Catch { Write-Warning &quot;ERROR RETRIEVING VALUE OR PROPERTY [$Property] BASED ON QUERY: [$AnchorProperty] = [$AnchorValue] FROM [$XMLFile]: $_&quot;; [int]$Return = $_ }
        return $Return
    }

I couldn’t think of an elegant way to retrieve the values I wanted without specifying some qualifiers.  For instance, if I wanted the price of all Eva Corets books

  • The AnchorPrperty would be: author
  • The AnchorValue would be: Corets, Eva
  • The Property would be price

This way I’m certain to get the results I’m looking for.

Is there a better way?  Maybe.  This is what I came up with that met my need.

Usage:

# These two work
Get-XMLValue -xmlFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -value bk108
Get-XMLValue -xmlFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -value bk107

# This deoesn't
Get-XMLValue -xmlFile &quot;C:\Users\Julius\Downloads\books.xml&quot; -value bk100

It may not be the most elegant solution but I’m hoping it’ll at least point you in the right direction should the need arise to get data from XML files.

 

Good Providence to you!