3

I have this PowerShell script in which values for an XML-file will be set by the script. This works perfectly fine when all child nodes are uniquely named. However, I'm adapting the XML-file to one where some nodes are repeated. Now I get an error in Powershell.

My question is, how to set the X'th node in XML to a certain value via PowerShell?

In short, my script works like the following:

cls

[xml] $xml1 = '<Lvl1>
                    <Lvl2>""</Lvl2>
                    <Lvl2>""</Lvl2>
                </Lvl1>' 

$xml1.Lvl1.Lvl2='./'

$xml1.Save("text.xml")

There is two times the same node (Lvl2), that's why I get the following error in PowerShell: "Cannot set "Lvl2" because only strings can be used as values to set XmlNode properties."

When I remove one (Lvl2) node, the script works like a charm.

Please advice.

Sunfile
  • 101
  • 1
  • 4
  • 22
  • see: https://stackoverflow.com/questions/42699869/change-xml-element-value-with-powershell – Luuk Feb 18 '20 at 12:46
  • @luuk, I don't see an answer to my question in the link you send me. I am able to change node property. However, when there is more then one node with the same name, I am stuk. – Sunfile Feb 18 '20 at 12:56

5 Answers5

3

You can always use .NET syntax, it works like a charm.

$xml1.SelectSingleNode('Lvl1/Lvl2[2]').InnerText='./'
Paweł Dyl
  • 8,888
  • 1
  • 11
  • 27
1
$xml1 = [xml]@'
    <Lvl1>
        <Lvl2>""</Lvl2>
        <Lvl2>""</Lvl2>
    </Lvl1>
'@

$xml1.GetElementsByTagName('Lvl2') | ForEach-Object { $_.InnerText = './' }

$xml1.Save("text.xml")

Get-Content -Path "text.xml" # debugging output

Debugging output: .\SO\60280990.ps1

<Lvl1>
  <Lvl2>./</Lvl2>
  <Lvl2>./</Lvl2>
</Lvl1>

Above approach works even for a bit more complicated input data e.g. as follows:

$xml1 = [xml]@'
<root>
    <Lvl1>
        <Lvl2>"a"</Lvl2>
        <Lvl2>"b"</Lvl2>
    </Lvl1>
    <Lvl0>
        <Lvl1>
            <Lvl2>"c"</Lvl2>
            <Lvl2>"d"</Lvl2>
        </Lvl1>
    </Lvl0>
</root>
'@
JosefZ
  • 28,460
  • 5
  • 44
  • 83
1

You can get the child nodes of Lvl1 as aray and then use the index to adjust the wanted node:

[xml] $xml1 = '<Lvl1>
                    <Lvl2>""</Lvl2>
                    <Lvl2>""</Lvl2>
                </Lvl1>' 

$lvl2Nodes = @($xml1.Lvl1.ChildNodes)

$lvl2Nodes[1].'#text' = "blah"  # updating the second childnode only

$xml1.Save("D:\text.xml")

Result:

<Lvl1>
  <Lvl2>""</Lvl2>
  <Lvl2>blah</Lvl2>
</Lvl1>
Theo
  • 57,719
  • 8
  • 24
  • 41
0
PS D:\TEMP> type lvl.xml
<Lvl1>
                    <Lvl2>"test1"</Lvl2>
                    <Lvl2>"test2"</Lvl2>
                </Lvl1>
PS D:\TEMP> $xml = New-Object XML
PS D:\TEMP> $xml.Load("lvl.xml")
PS D:\TEMP> $element =  $xml.SelectSingleNode("/Lvl1/Lvl2[2]")
PS D:\TEMP> $element.InnerText ="bla"
PS D:\TEMP> $xml.Save("lvl2.xml")
PS D:\TEMP> type lvl2.xml
<Lvl1>
  <Lvl2>"test1"</Lvl2>
  <Lvl2>bla</Lvl2>
</Lvl1>
PS D:\TEMP>

In the XmlPath (i.e. '/Lvl1/Lvl2[2]') the '[2]' refers to the second time this thing exists.

Luuk
  • 12,245
  • 5
  • 22
  • 33
0

Paweł Dyl's helpful answer provides a concise solution that bypasses PowerShell's adaptation of the XML DOM, which uses the familiar dot notation, in favor of using .NET methods directly.

However, PowerShell's dot notation is both convenient and uses familiar syntax, so it's worth sticking with it, where possible and appropriate.

Unfortunately, in the case at hand that is only possible with a hybrid approach, combining dot notation (using the element or attribute names as directly as property names) with native properties of the underlying System.Xml.XmlNode-based types:

Note: I'm using the following modified XML and variable name $xml in the commands below:

[xml] $xml = @'
<Lvl1>
  <Lvl2>a</Lvl2>
  <Lvl2>b</Lvl2>
  <Other><Stuff>c</Stuff></Other>
</Lvl1>
'@ 

To assign a value to the first Lvl2 child, you can use the following approach:

# Assign to the *first* Lvl2 element.
$xml.Lvl1['Lvl2'].InnerText = './'
  • Using the indexer (['Lvl2']) instead of dot notation (.Lvl2) utilizes the System.Xml.XmlElement type's type-native indexer to target the first child element by the given name.

  • That element's .InnerText property can then be assigned to.

If you need to assign to a child element with a given index, a different approach is needed, using the numeric (0-based) indexing of an XmlElement's .ChildNodes collection (a streamlined version of Theo's helpful answer):

# Assign to the *last* Lvl2 element.
$xml1.Lvl1.ChildNodes[-1].InnerText = 'last'

What doesn't work as of PowerShell [Core] 7.0:

Unfortunately, indexing directly into the child elements using dot notation alone does not work for assignments:

# DOESN'T WORK: PowerShell *quietly ignores* the assignment.
$xml.Lvl1.Lvl2[-1] = 'last' 

This surprising behavior has been reported in this GitHub issue.

However, getting a value this way (typically) works fine:

# OK
PS $xml.Lvl1.Lvl2[0]
a

Caveat:

If a named element happens to be the only child element and it itself has element children, indexing does not work, preventing the unified treatment of scalars and collections that PowerShell normally provides:

# DOES NOT WORK, because there is only a single 'Other' element
# *and* it has a child element.
PS $xml.Lvl1.Other[0]
 # !! No output

Surprisingly, PowerShell uses the type-native by name indexer to look for an element named '0' - which by definition fails, given that XML element names mustn't start with digits.

This GitHub suggestion proposes use of PowerShell's automatic positional indexing in this case, based on the argument being an integer.

mklement0
  • 382,024
  • 64
  • 607
  • 775