简体   繁体   中英

powershell xml sort nodes and replacechild

I'm trying to do something pretty simple with powershell and xml but haven't a bit of trouble. Basically I'm trying to take the following xml... and sort the machine elements by name. Then put them back into the XML so that I can save back to a file.

Sorting seems to be working if output the $new object however, during the replacechild it's complaining "Cannot convert argument "0", with value: "System.Object[]" for "ReplaceChild" to type "System.Xml.XmlNode" : "Cannot convert the "System.Object[]" to type "System.Xml.XmlNode".

If I do a Get-Member on both $orig and $new they both say they are of type XMLElement which I believe inherits from XMLNode.

What am I missing guys? Driving me nuts. Thanks for your help!

<company>
    <stuff>
    </stuff>
    <machines>
        <machine>
            <name>ca</name>
            <b>123</b>
            <c>123</c>
        </machine>
        <machine>
            <name>ad</name>
            <b>234</b>
            <c>234</c>
        </machine>
        <machine>
            <name>be</name>
            <b>345</b>
            <c>345</c>
        </machine>
    </machines>
    <otherstuff>
    </otherstuff>
</company>

[xml]$xml = (get-content Company.xml)
[XmlNode]$orig = $xml.Company.Machines
[XmlNode]$new = ($orig.Machine | sort Name )
$xml.Company.ReplaceChild($new, $orig)

There are various problems here. One is that your sort returns a list of xml elements rather than a single xml element. Another problem is that it returns the original xml elements rather than copies, so any manipulation of the xml DOM that you do using them will also affect the result.

Here's a simple way to get what you want. Sort in reverse order, and then insert each node in turn in front of the others. Each time you insert a node from the result of the sort it will automatically remove it from the original set of node:

[xml]$xml = @"
<company>
    <stuff>
    </stuff>
    <machines>
        <machine>
            <name>ca</name>
            <b>123</b>
            <c>123</c>
        </machine>
        <machine>
            <name>ad</name>
            <b>234</b>
            <c>234</c>
        </machine>
        <machine>
            <name>be</name>
            <b>345</b>
            <c>345</c>
        </machine>
    </machines>
    <otherstuff>
    </otherstuff>
</company>
"@
[System.Xml.XmlNode]$orig = $xml.Company.Machines
$orig.Machine | sort Name  -Descending |
  foreach { [void]$xml.company.machines.PrependChild($_) }
$xml.company.machines.machine

Edit: The pipeline can also be written with an ascending sort (as David Martin pointed out), and you can reduce typing by using the node in the variable:

$orig.Machine | sort Name | % { [void]$orig.AppendChild($_) }

In addition to the already excellent answers by Duncan and robert.westerlund I'd like to provide an example of how to do the sorting of xml elements in case the nodes are not always in the same path:

[xml]$xml = @"
<config>
    <namespace name='Some namespace'>
        <settings>
            <setting name='c setting' />
            <setting name='a setting' />
            <setting name='b setting' />
        </settings>
        <namespace name='Child namespace'>
            <settings>
                <setting name='z setting' />
                <setting name='y setting' />
                <setting name='x setting' />
            </settings>
        </namespace>
    </namespace>
</config>
"@

#[xml]$xml = Get-Content "PATH_TO_SOME_FILE.xml"

$xml.SelectNodes("//namespace/settings") | % { 
    $parentNode = $_; $_.Setting | Sort-Object -Property name | % { 
        $parentNode.AppendChild($_) | Out-Null
    } 
}

$xml.Save("PATH_TO_SOME_FILE.xml")

In this example, the settings node appears on different levels in the xml file and sorting is done with respect to this level.

The reason this doesn't work is that $xml.Company.Machines is a single XmlElement. To get the collection of machine elements, you need to use $xml.Company.Machines.Machine, so this is the one you want to sort.

However, the ReplaceChild method doesn't take a collection, so I'm guessing you'll have to remove all children and then append them back in the order you want them. Something like the following should work fine:

[xml]$xml = Get-Content .\Company.xml
$machines = $xml.company.machines
$orderedMachineCollection = $machines.machine | Sort Name
$machines.RemoveAll()
$orderedMachineCollection | foreach { $machines.AppendChild($_) } | Out-Null

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM