1

I'm retrieving a string of text from some XML data, and then trying to replace that string with something else. The code I'm using is:

$newstring = "mystring"
$bar = Get-Gpo -Name "My GPO"
$gpoid = $bar.Id.ToString()
$drivesxml = New-Object XML
$drivesxml.Load("\\mydomain.co.uk\sysvol\mydomain.co.uk\Policies\{gpoid}\User\Preferences\Drives\drives.xml")
$oldpath = $drivesxml.Drives.Drive.Properties.Path[0]
$oldlabel = $drivesxml.Drives.Drive.Properties.Label[0]
#1
[string]$newtemppath = $oldpath -replace "Oldstring$", "$newstring"
[string]$newtemplabel = $oldlabel -replace "^Oldstring", "$newstring"
#2
$drivesxml.Drives.Drive.Properties.Path[0] = $newtemppath
$drivesxml.Drives.Drive.Properties.Label[0] = $newtemplabel
#3
$drivesxml.Save("\\mydomain.co.uk\sysvol\mydomain.co.uk\Policies\{gpoid}\User\Preferences\Drives\drives.xml")

It retrieves the XML from sysvol fine, and at point #1 if I query $oldpath and $oldlabel they contain the expected text from the XML.

At #2 if I query $newtemppath and $newtemplabel the strings are returned as expected with the text amended so instances of "Oldstring" have been replaced by "mystring".

At #3 if I query $drivesxml.Drives.Drive.Properties.Path[0] and $drivesxml.Drives.Drive.Properties.Label[0] I'd expect them to return the same content as the $newtemppath and $newtemplabel variables, but instead they continue to return their original values.

After saving the XML if I query it again the content hasn't changed.

Can anyone see what I might be doing wrong with the assignment between #2 and #3?

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
Keith Langmead
  • 785
  • 1
  • 5
  • 16

2 Answers2

3

Powershell supports a "dot-path" syntax for navigating XML files that is very handy for reading data from an XML file—presumably to make it convenient to consume XML input or to use an XML config file.

The downside of that syntax is that it tries to return strings and arrays whenever it can. In your case, $drivesxml.Drives.Drive.Properties.Path[0] is a string, and not an XML node anymore, so whatever you assign to this value will be lost.

The trick is to stay with XML nodes. The easiest way to make this happen is to use XPath for navigation (it's more powerful than the dot-path syntax anyway):

$path = $drivesxml.SelectSingleNode('/Drives/Drive/Properties/Path')
$path.InnerText = $path.InnerText -replace "Oldstring$",$newstring

# ...

$drivesxml.Save($xmlPath)

You can also use .SelectNodes and a foreach loop to do multiple changes.


On an unrelated note, Powershell will do variable interpolation in double-quoted strings. This can clash with regular expressions, where the $ has its own meaning. In the case above there is no ambiguity, but it's better to get into the habit of using single-quoted strings for regular expressions:

$path.InnerText = $path.InnerText -replace 'Oldstring$',$newstring

A note on XML namespaces: If your XML file uses namespaces (declared by xmlns on the elements), you need to make those namespaces known before you can use XPath: Using PowerShell, how do I add multiple namespaces (one of which is the default namespace)?

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Beware that with this approach you need a namespace manager if the XML has namespaces (which I can practically guarantee for any kind of XML generated by Microsoft products). – Ansgar Wiechers Jul 17 '18 at 16:49
  • Good point, I'll add a link to an answer that details how that works. – Tomalak Jul 17 '18 at 16:50
  • 1
    Brilliant, thanks! Fortunately GPO .xml files don't appear to use namespaces. Turned out ".path" isn't an actual node which presumably didn't matter with dot-path, but does with XPath, so ended up using SelectNodes('/Drives/Drive/Properties'), and then $path[0].path and $path[0].label, and that works, but wouldn't have got there without you pointing me in the right direction. – Keith Langmead Jul 18 '18 at 11:23
  • 1
    @KeithLangmead Thanks for the feedback. Take-away for next time, add (a meaningful sample of) the XML file to the question, so we don't have to guess our way through the answer. (Speaking of which, I guess `path` is an attribute, they are addressed using `@` in XPath, i.e. `/Drives/Drive/Properties/@path`. Note that XPath is case-sensitive, whereas Powershell is generally not.) – Tomalak Jul 18 '18 at 12:05
  • Aha, that's good to know, thanks. Had a hunt online, but not helped by many people online conflating nodes and attributes! :) Fair point about including the XML, if the read part hadn't worked I definitely would have, but since it did I naively figured it wasn't necessary. – Keith Langmead Jul 18 '18 at 13:14
  • Well, attributes are nodes, too, so that's not actually a conflation. They are just a different type of node. Compare the inhertance hierarchies of [`XmlAttribute` (MSDN)](https://technet.microsoft.com/en-us/library/security/system.xml.xmlattribute) and [`XmlElement` (MSDN)](https://technet.microsoft.com/en-us/library/security/system.xml.xmlelement). – Tomalak Jul 18 '18 at 13:52
1

Just for completeness, the working code once I’d implemented Tomalak’s answer is.

$newstring = "mystring"
$bar = Get-Gpo -Name "My GPO"
$gpoid = $bar.Id.ToString()
$drivesxml = New-Object XML
$drivesxml.Load("\\mydomain.co.uk\sysvol\mydomain.co.uk\Policies\{gpoid}\User\Preferences\Drives\drives.xml")
$path=$drivesxml.SelectNodes('/Drives/Drive/Properties')    
$path[0].path = $path[0].path -replace 'Oldstring$',$newstring
$path[0].label = $path[0].label -replace '^Oldstring',$newstring
$drivesxml.Save("\\mydomain.co.uk\sysvol\mydomain.co.uk\Policies\{gpoid}\User\Preferences\Drives\drives.xml")

And the XML that it's querying for reference :

<?xml version="1.0" encoding="utf-8"?>
<Drives clsid="{8FDDCC1A-0C3C-43cd-A6B4-71A6DF30DA8C}">
  <Drive clsid="{935D1B74-9CB8-4e3c-9914-7DD559C7A417}" name="S:" status="S:" image="0" changed="2010-07-15 15:01:51" uid="{ACCCC2A9-809B-4CE3-9F2D-F4B4643B02F5}" bypassErrors="1">
    <Properties action="C" thisDrive="SHOW" allDrives="SHOW" userName="" path="\\mydomain.co.uk\GroupData$\Companies\mystring"     label="mystring - Shared drive" persistent="1" useLetter="1" letter="S" />
  </Drive>
  <Drive clsid="{935D1B74-9CB8-4e3c-9914-7DD559B8A417}" name="U:" status="U:" image="0" changed="2010-07-15 15:04:20" uid="{FA3F7F3C-A1A0-4618-89E1-88191C780A67}" bypassErrors="1">
    <Properties action="C" thisDrive="SHOW" allDrives="NOCHANGE" userName="" path="\\mydomain.co.uk\data\users\%username%" label="Personal drive (My documents)" persistent="1" useLetter="1" letter="U" />
  </Drive>
</Drives>
Keith Langmead
  • 785
  • 1
  • 5
  • 16
  • 1
    I would use something like `$drive_S = $drivesxml.SelectSingleNode("/Drives/Drive/Properties[@name = 'S:']")` and then `$drive_S.path = $drive_S.path -replace 'Oldstring$',$newstring`, this is a bit more straight-forward than using `.SelectNodes()` and blindly picking the first one with `[0]` - and it does not break when the order of the `` elements suddenly is the other way around for some reason. – Tomalak Jul 18 '18 at 12:14