1
$xml = [xml] @'
<?xml version="1.0" encoding="UTF-8"?>
<group>
    <product description="phone" id="1234/5678">
        <item name="apple" version="50" />
        <item name="banana" version="100" />
    </product>
    <product description="notebook" id="6666/7777">
        <item name="orange" version="150" />
    </product>
</group>
'@

$xml.group.product[0].item[0].name works (returns 'apple'), because the 1st product element has 2 item child elements.

However, $xml.group.product[1].item[0].name does not work (returns $null), because there is only one item element.

How can I reliably access the first item child element without having to know whether it happens to be the only one?

mklement0
  • 382,024
  • 64
  • 607
  • 775
Dukeyu
  • 67
  • 1
  • 7

1 Answers1

1

Your example xml isn't quite valid, so I'm going to use a slightly modified version:

$xml = [xml] @"
<?xml version="1.0" encoding="UTF-8"?>
<group>
  <product description="phone" id="1234/5678">
    <item name="apple" version="50" />
    <item name="banana" version="100" />
  </product>
  <product description="notebook" id="6666/7777">
    <item name="orange" version="150" />
  </product>
</group>
"@

In your scenario, there's a PowerShell feature called Member Enumeration which applies to your first product node and returns an array of all the child item nodes, but for the second product node it just returns the item node itself:

PS> $xml.group.product[0].item.GetType().FullName
System.Object[]

PS> $xml.group.product[1].item.GetType().FullName
System.Xml.XmlElement

As a result you can index into the array from the first product node, but not the XmlElement from the second, which gives the behaviour you've seen.

To fix this, what you could do is coerce the item nodes into an array so that you can index into it even if there's only one item:

PS> @($xml.group.product[0].item).GetType().FullName
System.Object[]

PS> @($xml.group.product[1].item).GetType().FullName
System.Object[]

PS> @($xml.group.product[1].item)[0].name
orange
mclayton
  • 8,025
  • 2
  • 21
  • 26
  • +1 for the right explanation and effective workaround, but that there shouldn't be a need for `@(...)`, because PowerShell generally allows indexing even into scalars (e.g., `(42[0])`), in an effort to unify handling of scalars and collections. There is currently a conflict with the type-native `XmlElement` indexer, which accepts _strings_ to access _child elements_, but I think this conflict can and should be resolved so that _numeric_ indexing works consistently. See [this GitHub issue](https://github.com/PowerShell/PowerShell/issues/11189). – mklement0 Nov 25 '19 at 16:45
  • @mklment0 - I've updated the examples to remove ```write-host```. I normally avoid the output stream and litter my code with ```write-host``` and ```return ``` instead to make it clear what's going on - too many late nights trying to find where a value is being unexpectedly output from a function which is calling something like ```StringBuilder.Append``` to remember :-). – mclayton Nov 25 '19 at 20:00
  • I appreciate the update, @mclayton. If you _know_ the true purpose of `Write-Host` and when its use is appropriate, it's obviously fine to use it, but it's better not to make that assumption on SO, especially for newcomers familiar with shells that require something like `echo` to print to the console _and_ output data; they often - understandably, but incorrectly - think that `Write-Host` is the PowerShell equivalent. – mklement0 Nov 25 '19 at 20:03