Jose
Jose

Reputation: 1461

Modify XML Parent Attributes iterating through Child attributes using powershell

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

Answers (2)

Parfait
Parfait

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

har07
har07

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

Related Questions