How to use InsertAfter with PowerShell - powershell

I have some xml files where I want to insert the contents of one xml file into another. I thought I'd use LastChild and the InsertAfter method to accomplish this. So far it's not working for me.
Here is the parent.xml file:
<manifest>
<manifestExecution>
<assetDetail>
<fileAsset fileAssetGuid="parentguid1">
<parentfile1 />
</fileAsset>
<fileAsset fileAssetGuid="parentguid2">
<parentfile2 />
</fileAsset>
</assetDetail>
</manifestExecution>
</manifest>
And here is the child.xml file:
<manifest>
<manifestExecution>
<assetDetail>
<fileAsset fileAssetGuid="childguid1">
<childfile1 />
</fileAsset>
</assetDetail>
</manifestExecution>
</manifest>
What I want to do is select the fileAsset node(s) from child.xml and insert into parent.xml after the last fileAsset node in parent.xml.
Here is my test code:
$parent = [xml] (Get-Content d:\temp\parent.xml)
$parentnode = $parent.manifest.manifestExecution.assetDetail
$child = [xml] (Get-Content d:\temp\child.xml)
$childnode = $child.manifest.manifestExecution.assetDetail.InnerXml
$parentnode.InsertAfter($childnode, $parentnode.LastChild)
Here is the error msg I'm getting:
Cannot convert argument "0", with value: "<fileAsset fileAssetGuid="childguid1"> <childfile1 /></fileAsset>", for "InsertAfter" to type "System.Xml.XmlNode": "Cannot conver
t the "<fileAsset fileAssetGuid="childguid1"><childfile1 /></fileAsset>" value of type "System.String" to type "System.Xml.XmlNode"."
At line:5 char:24
+ $parentnode.InsertAfter <<<< ($childnode, $parentnode.LastChild)
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
What am I doing wrong?

You need to iterate through $childnode's children, remove them from their parent, and import them into the new document context ($child and $parent are different XmlDocument instances) before appending to $parentnode.
This will append all fileAsset nodes from $childnode into $parentnode.
$parent = [xml](get-content d:\temp\parent.xml)
$parentnode = $parent.manifest.manifestexecution.assetdetail
$child = [xml](get-content d:\temp\child.xml)
$childnode = $child.manifest.manifestexecution.assetdetail
while ($childnode.haschildnodes) {
$cn = $childnode.firstchild
$cn = $childnode.removechild($cn)
$cn = $parentnode.ownerdocument.importnode($cn, $true)
$parentnode.appendchild($cn)
}
Fortunately, most of these methods return the same XmlNode or a new version of it, so the body of the while loop could chained together like this:
$parentnode.appendchild( $parentnode.ownerdocument.importnode( $childnode.removechild( $childnode.firstchild ), $true ))
InsertAfter(newChild,referenceChild) could also work, but would be done a little differently since it also needs a reference to the the node that it will be inserted after.

your first problem is that you're not getting an XML element, but a string. You need to get an XML node from your XML document, but the shorthand method you're using is guessing you want a string. Usually you can force it by explicitly casting it over to [System.Xml.XmlElement], but that doesn't always work. You can reliably get an element using "SelectSingleNode".
You've not hit your second problem yet, but it's just around the corner. Once you've got XML, it still won't work because it's from a different XML document, so you need to "Import" the node. You'll want to tweak this to get the XML to align the way you envision, but the code works.
$parentString = #"
<manifest>
<manifestExecution>
<assetDetail>
<fileAsset fileAssetGuid="parentguid1">
<parentfile1 />
</fileAsset>
<fileAsset fileAssetGuid="parentguid2">
<parentfile2 />
</fileAsset>
</assetDetail>
</manifestExecution>
</manifest>
"#
$childString = #"
<manifest>
<manifestExecution>
<assetDetail>
<fileAsset fileAssetGuid="childguid1">
<childfile1 />
</fileAsset>
</assetDetail>
</manifestExecution>
</manifest>
"#
$parent = [xml] ($parentString)
$parentnode = $parent.manifest.manifestExecution.assetDetail
$child = [xml] ($childString)
$xpath = '/manifest/manifestExecution/assetDetail'
$childnode = $child.SelectSingleNode($xpath)
Write-Host("So the child is $($childnode.OuterXML)")
$importedNode = $parent.ImportNode($childNode,$true)
Write-Host("And after importing: $($importedNode.OuterXML)")
$parentnode.InsertAfter($importednode, $parentnode.LastChild)
Write-Host("To finally yield: $($parent.OuterXML)")
Also, you may find you can use something like your original code if you cast it to XmlElement properly.
$childnode = [System.Xml.XmlElement]$child.manifest.manifestExecution.assetDetail.InnerXml

Related

Not able to Get the Last node of an xml node properly

I am having 2 diffrent xml file with. Currently im trying to get the Last xml node. In the 1st ouput im getting because empty or Null value since i have a commented line. Please help how to handle in this case.
#Below line Run firstfile.xml
$xmlfile = 'C:\Programs\MyTasks\TCPROD-29658\Test_1309\D_SimulationTest\firstfile.xml'
#Below line to Run secondfile.xml
#$xmlfile = 'C:\Programs\MyTasks\TCPROD-29658\Test_1309\D_SimulationTest\secondfile.xml'
$xmllastsitenode = [xml]::new()
$xmllastsitenode.Load($xmlFile)
$lastsite_id = $xmllastsitenode.fccconfig.fccdefaults.LastChild.id
write-host 'Print1'
write-host $lastsite_id
if (!$lastsite_id) { Write-Host "variable is null" }
if ($lastsite_id) { Write-Host "variable is NOT null" }
write-host 'Print2'
Below is the xml files im trying to run
1st XML file
<?xml version="1.0" encoding="UTF-8"?>
<fccconfig version="1.3.2">
<fccdefaults>
<property name="CacheLocation" value="C:/Users/Public/" overridable="true"/>
<site id="-1940805554" overridable="true">
<parentfsc address="http://abcdefgh:1234/" priority="0" />
</site>
<!--__ANT_MARK__-->
</fccdefaults>
<!-- default parentfsc - this is a marker that will be overwritten by the installer -->
<parentfsc address="xyzlmnopq:10010" priority="0" transport="lan"/>
</fccconfig>
Output:-
Print1
variable is null
Print2
2nd XML file
<?xml version="1.0" encoding="UTF-8"?>
<fccconfig version="1.3.2">
<fccdefaults>
<property name="CacheLocation" value="C:/Users/Public/" overridable="true"/>
<site id="-1940805554" overridable="true">
<parentfsc address="http://abcdefgh:1234/" priority="0" />
</site>
</fccdefaults>
<!-- default parentfsc - this is a marker that will be overwritten by the installer -->
<parentfsc address="xyzlmnopq:10010" priority="0" transport="lan"/>
</fccconfig>
Output:-
Print1
-1940805554
variable is NOT null
Print2
Latest Code which have tried meantime
#Below line Run firstfile.xml
$xmlfile = 'C:\Programs\MyTasks\TCPROD-29658\Test_1309\D_SimulationTest\firstfile.xml'
#Below line to Run secondfile.xml
#$xmlfile = 'C:\Programs\MyTasks\TCPROD-29658\Test_1309\D_SimulationTest\secondfile.xml'
$xmllastsitenode = [xml]::new()
$xmllastsitenode.Load($xmlFile)
$lastsite_id = $xmllastsitenode.fccconfig.fccdefaults.LastChild.id
write-host 'Print1'
write-host $lastsite_id
if (!$lastsite_id)
{
Write-Host "variable is null"
# New logic to handle firt xml file cases
#$somesitetag ---> Having some xml tag content here
$newSiteNode = $xmlcontent.ImportNode($somesitetag, $true)
$antmarkline = (Get-Content $xmlfile) | select-string -Pattern $antmark
$xmlcontent.fccconfig.fccdefaults.InsertAfter($newSiteNode,$antmarkline)
}
if ($lastsite_id)
{
Write-Host "variable is NOT null"
#Insterting some new nodes
#This Working as second file conetent works here
}
write-host 'Print2'
You need to ignore the comments. Check the output of this for the first XML file.
$settings = [System.Xml.XmlReaderSettings]::new()
$settings.IgnoreComments = $true
$xmlreader = [System.Xml.XmlReader]::Create('./Desktop/Test.xml', $settings)
$xmldoc = [System.Xml.XmlDocument]::new()
$xmldoc.Load($xmlreader)
$xmldoc.fccconfig.fccdefaults.LastChild

pass json as parameter in power shell script

I have made a function for creating new xml node.there are two parameters in my function on is a existing xml file reference and second one is element value.while running the script its showing an error
code
function createProviderNode($xmlData,$propertyValue){
Write-Host 'inside createProviderNode'
Write-Host ($propertyValue)
#[xml]$xmlData = get-content E:\powershell\data.xml
$newProviderNode = $xmlData.CreateNode("element","provider","")
$newProviderNode.SetAttribute("name",$propertyValue)
$xmlData.SelectSingleNode('providers').AppendChild($newProviderNode)
$xmlData.save("E:\powershell\data.xml")
}
did i miss anything in this code?
The error message implies that while you expected $xmlData to contain an object of type [xml] (System.Xml.XmlDocument) - i.e., an XML document - in reality it was a string ([string]).
In other words: When you called your createProviderNode function, the 1st argument you passed was a string, not an XML document (of type [xml]).
Typing your $xmlData parameter variable as [xml] solves this problem, as that will implicitly covert even a string argument to an XML document on demand - if possible.
A simplified example, using a script block in lieu of a function:
$xmlString = #'
<?xml version="1.0"?><catalog><book id="bk101"><title>De Profundis</title></book></catalog>
'#
# Note how $xmlData is [xml]-typed.
& { param([xml] $xmlData) $xmlData.catalog.book.title } $xmlString
The above yields De Profundis, demonstrating that the string argument was converted to an [xml] instance (which - thanks to PowerShell's type adaptation magic - makes the element names available as direct properties).
It is then safe to call the .CreateNode() method on $xmlData.
Well, you don't show your original XML format.
Why did you comment out that Get-Content? it will not work without it.
So, if we take the below example, it works as expected.
# Simple XML version
$SimpleXml = $null
$SimpleXml = #"
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<name>Apple</name>
<size>1234</size>
</configuration>
"#
# New node code
[xml]$XmlDoc = Get-Content -Path variable:\SimpleXml
$runtime = $XmlDoc.CreateNode("element","runtime","")
$generated = $XmlDoc.CreateNode("element","generatePublisherEvidence","")
$generated.SetAttribute("enabled","false")
$runtime.AppendChild($generated)
$XmlDoc.configuration.AppendChild($runtime)
$XmlDoc.save("$pwd\SimpleXml.xml")
Get-Content -Path "$pwd\SimpleXml.xml"
# Which creates this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<name>Apple</name>
<size>1234</size>
<runtime>
<generatePublisherEvidence enabled="false" />
</runtime>
</configuration>
Also Write-Host is never needed unless you are coloring screen output.
Write-Output is the default and automatically write to the screen, whether you specify Write-Output or not.
So, all of these to the same thing - output to the screen.
$SomeString = 'hello'
Write-Host $SomeString
Write-Output $SomeString
'hello'
"hello"
$SomeString
"$SomeString"
($SomeString)
("$SomeString")
$($SomeString)
# Results
hello
hello
hello
hello
hello
hello
hello
… yet, it's your choice.

Parsing XML and ignoring sections with powershell

I want to parse out the key fields and Data table information from here with PowerShell.
I only want the datatable name if there is a keyfield so in the example below I do not want CC:Attribute.
I also want to output things to a text file.
I want to have a text file that is created that holds the Data table name & Access as well as all the key fields and what they are.
This is the code I have so far:
[xml]$global:xmldata = get-content "C:\hackathon\Mfg.xml"
$xmldata2 = $xmldata.SchemaPackage.Tables
$SField = $xmldata2.DataTable.KeyFields | %{$_.StringField}
$Reffield = $xmldata2.DataTable.KeyFields | %{$_.ReferenceField}
$table = $xmldata2 | %{$_.DataTable}
Xml File:
<?xml version="1.0" encoding="utf-8"?>
<SchemaPackage Namespace="Mfg" xmlns="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataTable Name="CC::Attribute">
<DataFields>
</DataFields>
</DataTable>
<DataTable Name="PlannerCode" Access="WW">
<Licenses>Manufacturing, DemandManagement</Licenses>
<Flags>
</Flags>
<KeyFields>
<StringField Name="Value"/>
<ReferenceField Name="Site" Target="Core::Site" SetField="PlannerCodes"/>
</KeyFields>
<DataFields>
<StringField Name="Description"/>
</DataFields>
</DataTable>
</SchemaPackage>
Despite the edit you made, I can only get your XML to validate if I modify it slightly (cleaning the opening XML tag and removing the SchemaPackage namespace). Regardless,
if you're experiencing no issues with your XML import then it's fine.
Here I'm just constructing the XML object from a herestring because I haven't got it in a file on disk.
[xml]$xmldata = #"
<xml>
<DataTable Name="CC::Attribute">
<DataFields>
</DataFields>
</DataTable>
<DataTable Name="PlannerCode">
<Licenses>Manufacturing, DemandManagement</Licenses>
<Flags>
</Flags>
<KeyFields>
<StringField Name="Value"/>
<ReferenceField Name="Site" Target="Core::Site" SetField="PlannerCodes"/>
</KeyFields>
<DataFields>
<StringField Name="Description"/>
</DataFields>
</DataTable>
</xml>
"#
# Filter DataTable nodes for those with a KeyFields child node.
$DataTablesWithKeyFields = $xmldata.xml.DataTable | Where-Object { $_.KeyFields }
$DataTableName = $DataTablesWithKeyFields.Name
$StringFieldData = $DataTablesWithKeyFields.KeyFields.ReferenceField
$ReferenceFieldData = $DataTablesWithKeyFields.KeyFields.ReferenceField
I'm not sure if that's what you're after. $DataTablesWithKeyFields could be an array depending on your XML file so you may need to loop it to extract the information you require.
Since we're working with XML, one of the querying options is XPath!
You can select only DataTable nodes that have a KeyFields child with the following XPath expression:
/SchemaPackage/DataTable[KeyFields]
You can use Select-Xml:
Select-Xml -Path C:\hackathon\Mfg.xml -XPath /SchemaPackage/DataTable[KeyFields] |Select-Object -Expand Node
or pass the expression as an argument to the SelectSingleNodes() method:
[xml]$xmldata = Get-Content C:\hackathon\Mfg.xml
$xmldata.SelectNodes('/SchemaPackage/DataTable[KeyFields]')

Why Error "System.String does not contain a method named 'AppendChild'"

Why does this PowerShell Script not work:
[xml]$xml = '<products></products>'
$newproduct = $xml.CreateElement('product')
$attr = $xml.CreateAttribute('code')
$attr.Value = 'id1'
$newproduct.Attributes.Append($attr)
$products = $xml.products
$products.AppendChild($newproduct)
Error is
Method invocation failed because [System.String] does not contain a method named 'AppendChild'.
At line:1 char:1
+ $products.AppendChild($newproduct)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
If I replace
$products = $xml.products
by
$products = $xml.SelectSingleNode('//products')
it will work, but I'd like to know why first syntax does not work because it is illogical for me. $xml.products should be a valid XML object and thus provide the method AppendChild().
$xml.products does not reference the products node itself, but the contents of the products node. Since products is empty, it evaluates to an empty string.
To get to the products node you could also use:
$Products = $xml.FirstChild
$Products.AppendChild($newproduct)
or, more specifically:
$Products = $xml.ChildNodes.Where({$_.Name -eq "products"}) |Select-Object -First 1
$Products.AppendChild($newproduct)
But SelectSingleNode() will probably serve you just fine
$xml.configuration.SelectSingleNode("connectionStrings").AppendChild($newnode)
would work fine, but an interesting & dirty hack: given this empty node:
<connectionStrings>
</connectionStrings>
the script
$xml.configuration.connectionStrings.AppendChild($newnode)
gives a "Method invocation failed because [System.String] does not contain a method named 'AppendChild'" error
but this:
<connectionStrings>
<!-- random comment -->
</connectionStrings>
works perfectly fine

Unable to update a value in msbuild proj file using powershell

I am trying to read a .csproj file using powershell by converting it into xml file to increment WPF app version value. I am able to read the value without any issue and incremented the version value but when I try to save the file with new value, the value doesn't get saved. The code I pasted in this question is to update the QA details.
I am trying to read the below file. How do i update and save file
<Choose>
<When Condition=" '$(BuildEnvironment)' == 'QA' ">
<PropertyGroup>
<ApplicationVersion>1.0.0.1</ApplicationVersion>
<PublishDirectory>C:\TestDirectory</PublishDirectory>
<InstallUrl>http://testurl/testapp</InstallUrl>
<ProductName>Test1.QA</ProductName>
<PublishAssemblyName>Test1.QA</PublishAssemblyName>
</PropertyGroup>
<ItemGroup>
<Tokens Include="ApplicationManifestFileName">
<ReplacementValue>Test1.QA.application</ReplacementValue>
<Visible>false</Visible>
</Tokens>
</ItemGroup>
</When>
<When Condition=" '$(BuildEnvironment)' == 'Production' ">
<PropertyGroup>
<ApplicationVersion>1.0.0.0</ApplicationVersion>
<PublishDirectory>C:\TestDirectory2</PublishDirectory>
<InstallUrl>http://test2url/test2app/</InstallUrl>
<ProductName>Test=2.Prod</ProductName>
<PublishAssemblyName>Test2.Prod</PublishAssemblyName>
</PropertyGroup>
<ItemGroup>
<Tokens Include="ApplicationManifestFileName">
<ReplacementValue>Tes2.Prod.application</ReplacementValue>
<Visible>false</Visible>
</Tokens>
</ItemGroup>
</When>
$Test1QAMSBuildFile = 'C:\Directory\Test.csproj'
[xml]$Test1QABuildVersion = Get-Content $Test1QAMSBuildFile
$CurrentQAVersion= $Test1QABuildVersion.Project.Choose.when.PropertyGroup | ? { $_.ProductName -eq 'Test1.QA' } | Select-Object -Property ApplicationVersion
$PropertyVersion= $CurrentQAVersion.ApplicationVersion
$UpdateVersion= $PropertyVersion.split(".")
$major= [int] ($Updateversion[0])
$minor= [int] ($Updateversion[1])
$patch= [int] ($Updateversion[2])
$revision=[int] ($Updateversion[3])
$newrevisionversion= $revision+1
$newVersion =( [string] $major )+ "."+ ( [string] $minor) +"."+ ([string]$patch ) +"."+ ([string]$newrevisionversion )
$UpdateVersion ="$newVersion"
$TestQABuildVersion.Save("Test1QAMSBuildFile")`
After I added the closing </Choose> I removed .Project from line 3, so you may need to alter what I give you to properly apply to your file.
You never updated $TestQABuildVersion, only some other things that referenced it. What you probably want to do is:
$Test1QAMSBuildFile = 'C:\Directory\Test.csproj'
[xml]$Test1QABuildVersion = Get-Content $Test1QAMSBuildFile
$PropertyVersion = $Test1QABuildVersion.Choose.when.PropertyGroup | ? { $_.ProductName -eq 'Test1.QA' } | Select -Expand ApplicationVersion
$UpdateVersion = $PropertyVersion.split(".")
$UpdateVersion[3] = 1+$UpdateVersion[3]
$newVersion = $UpdateVersion -join "."
$Test1QABuildVersion.Choose.when.PropertyGroup | ? { $_.ProductName -eq 'Test1.QA' } | %{$_.ApplicationVersion = $newVersion}
$TestQABuildVersion.Save($Test1QAMSBuildFile)
Below is code that will work on the example you have given. The main thing you need to keep in mind is that you are dealing with an xml object rather than a powershell object. Selecting the node to be updated requires using SelectSingleNode and specifying an x-path. Using the xml InnerText property allows you to get and set the value. The code below has been tested with the Test.csproj file you provided.
$Test1QAMSBuildFile = 'C:\Directory\Test.csproj'
[xml]$Test1QABuildVersion = Get-Content $Test1QAMSBuildFile
$node = $Test1QABuildVersion.SelectSingleNode("/Choose/When/PropertyGroup/ApplicationVersion[../ProductName = 'Test1.QA']")
$PropertyVersion= $node.InnerText
$UpdateVersion= $PropertyVersion.split(".")
$UpdateVersion[3] = (($UpdateVersion[3] -as [int]) + 1).ToString()
$newVersion = $UpdateVersion -join '.'
$node.InnerText = $newVersion
$Test1QABuildVersion.Save($Test1QAMSBuildFile)
In the case of a real csproj file you will also have to deal with namespaces since the Project element specifies a namespace. I've done bulk modifications to csproj files in the past and it involved quite a learning curve.