2

I am trying to write a script that starts with the most inner XML element and loops up the XML node until it gets to a specific node:

For example:

<bookstore>
  <book category="cooking">
    <title lang="en">Everyday Italian</title>
    <author>Giada De Laurentiis</author>
    <year>2005</year>
    <price>30.00</price>
  </book>
  <book category="children">
    <title lang="en">Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
  </book>
  <book category="web">
    <title lang="en">Learning XML</title>
    <author>Erik T. Ray</author>
    <year>2003</year>
    <price>39.95</price>
  </book>
</bookstore>

Let's say my pointer is on the element:

<year>2003</year>

and I want to loop trough the XML object until I get to the element ( note , that I don't know the names of any other elements except the root one, and don't know the size of the tree, this one is just an abstract example). With each iteration I want my pointer to be stored in an array as an element name and then move to the parent node, the iteration should continue until it gets to the root element (included).

I tried several while loops and used the xmlElement.ParentNode. My issue always ends up at not knowing how to move to the parent element of the current pointer because the start of the iteration always points to the original element.

This is my script:

    [xml]$File = Get-Content -Path $Path
    # Write-Host($File.ManuScript.properties)
 
    $nodes = (Select-Xml -Xml $File -XPath ("//*[starts-with(@value,'Carrier_')]"))
    # write-host($nodes.GetType())
    # 
   
    foreach ($node in $nodes) {
        [string] $nodeName = $node.Node.get_name()
     
        # Write-Host("*********")
        # Write-Host($nodeName)
        # Write-Host("++++++++++")
        $XpathElmArr = [System.Collections.ArrayList]@()
        $XpathElmArr.Insert(0,$nodeName);

        while ($node.Node.get_name() -ne "topic"){
            $tempNode = $node.ParentNode
            $XpathElmArr.Insert(0,$tempNode.get_name());
            $tempNode = $tempNode.ParentNode
            Write-Host($node.Node.get_name())
        }
        Write-Host($XpathElmArr)
    }

Appreciate any help.

dro dro
  • 21
  • 2
  • The best way is to use Descendants and go forward (not backwards). – jdweng Jan 30 '23 at 17:33
  • 1
    Does the sample xml match your actual document? Your code mentions ```Carrier_```, ```topic``` and ```Manuscript``` that don't appear in your xml... – mclayton Jan 30 '23 at 17:42
  • The sample is just a sample to demonstrate the goal, going up the stream of the XML node. – dro dro Jan 30 '23 at 18:08
  • 1
    Ok, but if I try to run your sample code locally it’s not going to do anything useful with your test data. You’re more likely to get a useful answer if you make it easier for people to reproduce the problem locally with relevant test data… – mclayton Jan 30 '23 at 18:48
  • As an aside: `Write-Host(...)` -> `Write-Host ...`. PowerShell functions, cmdlets, scripts, and external programs must be invoked _like shell commands_ - `foo arg1 arg2` - _not_ like C# methods - `foo('arg1', 'arg2')`. If you use `,` to separate arguments, you'll construct an _array_ that a command sees as a _single argument_. See [this answer](https://stackoverflow.com/a/65208621/45375) for more information. `Write-Host` is typically the wrong tool to use, unless the intent is to write _to the display only_ - see [this answer](https://stackoverflow.com/a/50416448/45375). – mklement0 Jan 30 '23 at 19:18

1 Answers1

0

Building on your sample XML:

Select-Xml -LiteralPath $Path -XPath "//*[. = '2003']" | 
  ForEach-Object {
    $node = $_.Node
    # Walk the parent nodes until the document (root) element is reached
    # and collect the element names.
    [array] $elementNames =
      do {
        $node.get_Name()
      } while (($node = $node.ParentNode) -isnot [xml])
    # Reverse the array's elements, so that the element names
    # are listed starting with the document element.
    [Array]::Reverse($elementNames)

    # Output it as a string list with "/" as the separator
    # The result is an XPath path, albeit not necessarily specific
    # to the node at hand, and handling namespaces would require
    # additional work.
    '/' + ($elementNames -join '/')
  }

Output:

/bookstore/book/year

As for the do { ... } while (...) loop used to walk the chain of ancestral nodes (XML elements):

  • $node.get_Name() gets and implicitly outputs the name of the element node at hand, in each loop iteration.

  • Condition (($node = $node.ParentNode) -isnot [xml]):

  • Using the do { ... } while (...) loop as an expression, as the operand of the assignment to variable $elementNames, implicitly captures the loop's outputs, which PowerShell collects in an [array] ([object[]]) for you, if there are two or more outputs. The [array] type constraint ([array] $elementNames = ...) ensures that an array is constructed even in the event that there's only one output from the loop (which by default is captured as-is).

mklement0
  • 382,024
  • 64
  • 607
  • 775