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

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s