Reputation: 1461
So I have the following xml (items.xml) and I want to find the attributes of the child node item iterate through the attributes and if I find similar attributes at parent node level replace the same with the child node attributes and remove the child attributes other than the name.
<items>
<model type="model1" name="default" price="12.12" date="some_value">
<PriceData>
<item name="watch" price="24.28" date="2013-12-01" />
</PriceData>
</model>
<model type="model2" name="default" price="12.12" date="some_value">
<PriceData>
<item name="toy" price="22.34" date="2013-12-02"/>
</PriceData>
</model>
<model type="model3" name="default" price="12.12" date="some_value">
<PriceData>
<item name="bread" price="24.12" date="2013-12-03"/>
</PriceData>
</model>
</items>
The final xml should look like this
<items>
<model type="model1" name="watch" price="24.28" date="2013-12-0">
<PriceData>
<item name="watch" />
</PriceData>
</model>
<model type="model2" name="toy" price="22.34" date="2013-12-02">
<PriceData>
<item name="toy" "/>
</PriceData>
</model>
<model type="model3" name="bread" price="24.12" date="2013-12-03">
<PriceData>
<item name="bread" />
</PriceData>
</model>
</items>
I'm able to get the attributes at child level, but I'm unable to traverse back to the parent node from the child level.
Following is the code that I tried to get to the parent nodes
[xml]$model = get-content items.xml
$model.SelectNodes("//item/@*")
Output:
#text
-----
watch
24.28
2013-12-01
toy
22.34
2013-12-02
bread
24.12
2013-12-03
$model.SelectNodes("//item/@*") | foreach {write-host $_.parentnode}
No Output:
$model.SelectNodes("//item/@*") | foreach {write-host $_.parentnode.parentnode}
No Output:
I can get the attribute names of the child node as follows:
$model.SelectNodes("//item/@*") | foreach {write-host $_.name}
Output:
PS C:\BIOS_Work_Dir\Kit_Manifest_test> $model.SelectNodes("//item/@*") | foreach {write-host $_.name}
name
price
date
name
price
date
name
price
date
Now for each attribute, I just need to go back to the parent node, check if similar attribute exists and replace it with the child node attribute
So, I'm looking for something like
$model.SelectNodes("//item/@*") | foreach {($_.name).parentnode.parentnode.($_.name)} | <some code to replace parentnode attribute with child attribute>
Then to remove the child attributes something like
$model.SelectNodes("//item/@*") | where {$_.name -notlike "name"} | foreach {$_.Removeattribute()}
and if both these can be done in one single command that would be awesome
Maybe I'm also trying to do a lot of things in a single line
Any pointers are greatly appreciated! Not really sure what am I doing wrong here as powershell does not throw an error for parent node usage but just does not print anything. Any help is amazing from all you experienced programmers!!
Upvotes: 1
Views: 1819
Reputation: 107587
Since your title mentions Modify XML, consider XSLT, the special purpose language designed solely to transform XML files. Specifically, you can run the Identity Transform (copy document as is) and then keep only the attributes that match attribute names of ancestor::model
(grandparent) using the <xsl:when>
and <xsl:otherwise>
conditionals. PowerShell can create an object of .NET Framework class, System.Xml.Xsl.XslCompiledTransform, to run XSLT 1.0 scripts.
XSLT (save as .xsl to be passed as argument in PowerShell)
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes" method="xml"/>
<xsl:strip-space elements="*"/>
<!-- Identity Transform -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Conditionally Keep Item Attributes -->
<xsl:template match="item/@*[name()!='name']">
<xsl:variable name="item_attr" select="name()"/>
<xsl:choose>
<xsl:when test="ancestor::model/@*[name()=$item_attr]"/>
<xsl:otherwise><xsl:copy/></xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:transform>
PowerShell (general script for any XML input and XSLT 1.0 script)
param ($xml, $xsl, $output)
if (-not $xml -or -not $xsl -or -not $output) {
Write-Host "& .\xslt.ps1 [-xml] xml-input [-xsl] xsl-input [-output] transform-output"
exit;
}
trap [Exception]{
Write-Host $_.Exception;
}
$xslt = New-Object System.Xml.Xsl.XslCompiledTransform;
$xslt.Load($xsl);
$xslt.Transform($xml, $output);
Write-Host "generated" $output;
Read-Host -Prompt "Press Enter to exit";
Command line call
Powershell.exe -File "C:\Path\To\PowerShell\Script.ps1"^
"C:\Path\To\Input.xml" "C:\Path\To\XSLTScript.xsl" "C:\Path\To\Ouput.xml"
Output
<?xml version="1.0" encoding="utf-8"?>
<items>
<model type="model1" name="default" price="12.12" date="some_value">
<PriceData>
<item name="watch" />
</PriceData>
</model>
<model type="model2" name="default" price="12.12" date="some_value">
<PriceData>
<item name="toy" />
</PriceData>
</model>
<model type="model3" name="default" price="12.12" date="some_value">
<PriceData>
<item name="bread" />
</PriceData>
</model>
</items>
Upvotes: 3
Reputation: 89285
You can get parent element from an attribute via OwnerElement
property. So this is one possible way to get the desired output :
$model.SelectNodes("//item/@*") |
ForEach {
# set attributes value on `model` element
$_.OwnerElement.ParentNode.ParentNode.SetAttribute($_.LocalName, $_.Value)
# remove attributes except `name` from `item` element
If ($_.LocalName -ne "name") { $_.OwnerElement.RemoveAttribute($_.LocalName) }
}
Upvotes: 2