Gordon
Gordon

Reputation: 6873

SelectSingleNode relative to root node on passed XML

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.

Upvotes: 0

Views: 489

Answers (1)

Prophet
Prophet

Reputation: 33361

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'

Upvotes: 1

Related Questions