0

Given xml of

$xml = [Xml]@"
<Package>
    <Copy_Ex>
    </Copy_Ex> 
    <Move_Ex> 
        <Rules>
            <Rule>
                <Property>os</Property> 
                <Operator>-eq</Operator>
                <Value>Windows10</Value>
            </Rule>
        </Rules>
        <Task>
            <Rules>
                <Rule>
                    <Property>lastWriteTime</Property>                          
                    <Operator>-gt</Operator>
                    <Value>6W</Value>
                </Rule>
            </Rules>
        </Task>
    </Move_Ex>
</Package>
"@

I want to be able to select a single node from the xml, say <Move_Ex>, and pass it to a function, then independently access the two <Rules> nodes. I can do that now with

function Test {
    param (
        [Xml.XmlElement]$task
    )
    if ($nodeRules = $task.SelectSingleNode('//Rules')) {
        Write-PxXmlToConsole $nodeRules
    }
    if ($taskRules = $task.SelectSingleNode('//Task/Rules')) {
        Write-PxXmlToConsole $taskRules
    }
}
$task = $xml.SelectSingleNode('//Move_Ex')
Test $task

But // is searching the entire document, NOT just the [Xml.XmlElement] that I passed. So if I have a different <Rules> node in the <Copy_Ex> node, THAT is what gets returned by the first SelectSingleNode(). What I think I need is a way to identify the root node of the passed element, not the entire document. But I can't seem to find a way to do that consistently. My understanding is that while // finds the sequence of nodes anywhere in the document, / only finds it relative to the root node. Which should mean that

$task = $xml.SelectSingleNode('/Move_Ex')

finds only that <Move_Ex> in the root, and if I had another one somewhere else I would be fine. However, that doesn't return anything at all, which has me worried I don't really understand how either / or // works, which makes the chances of getting what I need to work unlikely. I have looked at the documentation for [Xml.XmlElement] and GetElementsByTagName() seems to be finding just the elements in my passed element. Write-Host "$($task.GetElementsByTagName('Rules').Count)" in the function returns a 2, so not finding any <Rules> node I have put in <Copy_Ex>. I also tried just dot referencing things, so

Write-PxXmlToConsole $task.Rules
Write-PxXmlToConsole $task.Task.Rules

And that seems to be working also. But again, ONLY when the element being passed is selected with // rather than /, and I worry that in a much more complex XML with hundreds of <Rules> nodes I won't be getting consistent results.

So, two questions...

1: What is the difference between / and // in this situation, and why isn't /Move_Ex working as "expected" in $task = $xml.SelectSingleNode('/Move_Ex')?

2: Is the dot referencing approach the "correct" way to limit my access to just the passed Element? Or is there another/better way?

I should note here that I did verify that // does break down if make the XML more realistic. So with this XML

$xml = [Xml]@"
<Package>
    <Copy_Ex>
        <Rules>
            <Rule>
                <Property>os</Property> 
                <Operator>-eq</Operator>
                <Value>WindowsXP</Value>
            </Rule>
        </Rules>
        <PreTask>
            <Move_Ex> 
                <Rules>
                    <Rule>
                        <Property>os</Property> 
                        <Operator>-eq</Operator>
                        <Value>WindowsVista</Value>
                    </Rule>
                </Rules>
                <Task>
                    <Rules>
                        <Rule>
                            <Property>lastWriteTime</Property>                          
                            <Operator>-gt</Operator>
                            <Value>8W</Value>
                        </Rule>
                    </Rules>
                </Task>
            </Move_Ex>
        </PreTask>
    </Copy_Ex> 
    <Move_Ex> 
        <Rules>
            <Rule>
                <Property>os</Property> 
                <Operator>-eq</Operator>
                <Value>Windows10</Value>
            </Rule>
        </Rules>
        <Task>
            <Rules>
                <Rule>
                    <Property>lastWriteTime</Property>                          
                    <Operator>-gt</Operator>
                    <Value>6W</Value>
                </Rule>
            </Rules>
        </Task>
    </Move_Ex>
</Package>
"@

Where I need to select only the <Move_Ex> in the <Package> node

$task = $xml.SelectSingleNode('/Move_Ex')

selects nothing, while

$task = $xml.SelectSingleNode('//Move_Ex')

selects the <Move_Ex> node nested in <Copy_Ex>, which is not at all what I want and need.

Note that all Write-PxXmlToConsole does is exactly that.

function Write-PxXmlToConsole ($xml) {
    $stringWriter = New-Object System.IO.StringWriter
    $xmlWriter = New-Object System.Xml.XmlTextWriter $stringWriter
    $xmlWriter.Formatting = "indented"
    $xml.WriteTo($xmlWriter)
    $xmlWriter.Flush()
    $stringWriter.Flush()
    Write-Host $stringWriter.ToString()
    Write-Host
}

EDIT: With respect to my first question, and based on what @Prophet has already said, I realized I was conflating Root NODE and Root ELEMENT, and now I see this in my related links. Some good additional info about Root node, root element, document element, etc.

Gordon
  • 6,257
  • 6
  • 36
  • 89

1 Answers1

1

Maybe I'm missing something or misunderstanding your question, but it seems to me that the answer is very simple:
The difference between / and //:
/ will go to the direct child of the passed argument while // will look for any element below the passed argument.
By default XPath searches starting from the root node. This is why '/Move_Ex' returns nothing. There is no Move_Ex element directly below the root.
To start searching from current node, not from the root, you should put a dot . at the prefix of the XPath expression. So, to get the Rules nodes inside the passed element you should use this XPath: '//.Rules'

Prophet
  • 32,350
  • 22
  • 54
  • 79
  • I wonder if this is a PowerShell specific thing? I have never seen that approach mentioned before, and when I try it now I get `Exception calling "SelectSingleNode" with "1" argument(s): "'//.Rules' has an invalid token."` As for the root node, I think I am understanding better now. I THOUGHT package was the root node. But is it really that there is a root node, which can only contain one node, Package in this case? `$task = $xml.SelectSingleNode('/Package/Move_Ex')` seems to work as if that's the case. Also, `'./Rules'` seems to work as expected. – Gordon Aug 08 '21 at 08:13
  • 1
    Interestingly, now that I can consistently get the right `` node with `'./Rules'`, I just did a Measure-Command on a 1000 iteration loop, and it seems that dot notation is about twice as fast. Curious to see if that holds true or changes when dealing with very large XML files. – Gordon Aug 08 '21 at 08:19
  • I have no idea if this is PowerShell issue. I do not familiar with PowerShell. I know, I hope, something about XPath. Package is really the root node here, but to select any other element using `/` you should define the path to it explicitly. F.e. `/Package/Move_Ex` or, alternatively just `//Move_Ex` – Prophet Aug 08 '21 at 08:22
  • I think on both counts PowerShell is doing something different. I think `./Rules` is how PS does it, to make it similar to relative path in the file system. And I am still not able to get `//Rules` to work. It doesn't find the rules in ``, it consistently finds the first ``, so when I have one nested in `` it finds that. Only `$task = $xml.SelectSingleNode('/Package/Move_Ex')` finds the one in root, when root is specifically named. :( That said, it's not like MS is known for following standards, or being consistent. :) – Gordon Aug 08 '21 at 08:37
  • Maybe it depends on method you are using to get the results? Again, I have no idea about PowerShell, I'm coming from Selenium world. So there, we have `findElement` method returning a single element. In case there are multiple elements on the page matching the passed locator it will return the first one, while `findElements` method will return a list containing all the matching elements. – Prophet Aug 08 '21 at 09:03
  • So, did my answer resolved your problem or at least helped? – Prophet Aug 08 '21 at 12:37
  • your answer definitely got me thinking, and led me to try `./Rules` which seems to be working. I would just love to have someone who knows the ins and outs of the PowerShell implementation chime in. I just don't trust Microsoft to not leave some surprises. – Gordon Aug 08 '21 at 17:10