0

I am writing a powershell script to create a xml file which i later feed into azure pipeline. Issue is this generate an encoded output with < , > being converted, which is not in a correct xml format enter image description here I understand this is related to automatic encoding. Some help to prevent this is appreciated

$myitems =
@([pscustomobject]
@{AssertName="Joe";TestPass=$true;},
[pscustomobject]
@{AssertName="Sue";TestPass=$false;},
[pscustomobject]
@{AssertName="Cat";TestPass=$true;})

Set-Content $path <?xml version="1.0"?><testsuites></testsuites>'

$xml = New-Object XML
$xml.Load($path)
$element =  $xml.SelectSingleNode("testsuites")
$innerText=""
foreach ($item in $myitems )
{
  $innerText=$innerText + '<testsuite errors="0" failures="0" id="0" name="$item.AssertName"  tests="1"><testcase classname="some.class.name" name="Test1" time="123.345000"/></testsuite>'
}

[xml] $xml = Get-Content -Raw $path
$xml.testsuites = $innerText
$xml.Save($path)
Heshan
  • 772
  • 8
  • 22
  • [What's so bad about building XML with string concatenation?](https://stackoverflow.com/q/3034611/1422451) – Parfait Aug 25 '22 at 00:48

2 Answers2

3

I strongly suggest to avoid working with raw XML strings and instead build the whole XML document element by element, using .NET API. This way you can just write any data as-is and the API makes sure of the proper XML encoding.

In general there are two kind of API for building XML:

  • DOM-based, e. g. using XmlDocument (type accelerator [xml]). This one is easiest to use, but is comparatively slow and stores the whole document in memory, which can be an issue for really large documents.
  • Stream-based, e. g. using XmlWriter. This is the fastest way and has the lowest memory footprint. It is more cumbersome to use, as you have to take care that elements are properly closed. Also you can't create the elements out of order, they are written in the order you call the API.

DOM-based solution

$myitems = @(
    [pscustomobject] @{AssertName="Joe";TestPass=$true}
    [pscustomobject] @{AssertName="Sue";TestPass=$false}
    [pscustomobject] @{AssertName="Cat";TestPass=$true}
)

$xml = [xml]::new()
$null = $xml.AppendChild( $xml.CreateXmlDeclaration('1.0', 'utf-8', $null) )
$root = $xml.AppendChild( $xml.CreateElement('testsuites') )

foreach ($item in $myitems )
{
    $testSuite = $root.AppendChild( $xml.CreateElement('testsuite') )
    $testSuite.SetAttribute('errors', 0)
    $testSuite.SetAttribute('failures', 0)
    $testSuite.SetAttribute('id', 0)
    $testSuite.SetAttribute('name', $item.AssertName)
    $testSuite.SetAttribute('tests', 1)

    $testCase = $testSuite.AppendChild( $xml.CreateElement('testcase') )
    $testCase.SetAttribute('classname', 'some.class.name')
    $testCase.SetAttribute('name', 'Test1')
    $testCase.SetAttribute('time', '123.345000')
}

$xml.Save( "$PSScriptRoot\test.xml" )

Output:

<?xml version="1.0" encoding="utf-8"?>
<testsuites>
  <testsuite errors="0" failures="0" id="0" name="Joe" tests="1">
    <testcase classname="some.class.name" name="Test1" time="123.345000" />
  </testsuite>
  <testsuite errors="0" failures="0" id="0" name="Sue" tests="1">
    <testcase classname="some.class.name" name="Test1" time="123.345000" />
  </testsuite>
  <testsuite errors="0" failures="0" id="0" name="Cat" tests="1">
    <testcase classname="some.class.name" name="Test1" time="123.345000" />
  </testsuite>
</testsuites>

Stream-based solution

$path = "$PSScriptRoot\test.xml"

$myitems = @(
    [pscustomobject] @{AssertName="Joe";TestPass=$true}
    [pscustomobject] @{AssertName="Sue";TestPass=$false}
    [pscustomobject] @{AssertName="Cat";TestPass=$true}
)
    
$writerSettings = [Xml.XmlWriterSettings] @{
    Encoding = [Text.Encoding]::UTF8
    Indent = $true
    IndentChars = "`t"
    WriteEndDocumentOnClose = $true  # Write document end tag automatically 
}
$writer = [xml.XmlWriter]::Create( $path, $writerSettings )

$writer.WriteStartDocument()   # writes the XML declaration
$writer.WriteStartElement('testsuites')

foreach ($item in $myitems )
{
    # Indentation is used to show the nesting of the XML elements
    $writer.WriteStartElement('testsuite')
        $writer.WriteAttributeString('errors', 0)
        $writer.WriteAttributeString('failures', 0)
        $writer.WriteAttributeString('id', 0)
        $writer.WriteAttributeString('name', $item.AssertName)
        $writer.WriteAttributeString('tests', 1)
        $writer.WriteStartElement('testcase')
            $writer.WriteAttributeString('classname', 'some.class.name')
            $writer.WriteAttributeString('name', 'Test1')
            $writer.WriteAttributeString('time', '123.345000')
        $writer.WriteEndElement()
    $writer.WriteEndElement()
}

# Very important - writes document end tag and closes the file
$writer.Dispose()  

Output:

<?xml version="1.0" encoding="utf-8"?>
<testsuites>
    <testsuite errors="0" failures="0" id="0" name="Joe" tests="1">
        <testcase classname="some.class.name" name="Test1" time="123.345000" />
    </testsuite>
    <testsuite errors="0" failures="0" id="0" name="Sue" tests="1">
        <testcase classname="some.class.name" name="Test1" time="123.345000" />
    </testsuite>
    <testsuite errors="0" failures="0" id="0" name="Cat" tests="1">
        <testcase classname="some.class.name" name="Test1" time="123.345000" />
    </testsuite>
</testsuites>
zett42
  • 25,437
  • 3
  • 35
  • 72
2

The way to do this is create an new xml document from your string and import the concerned node (ImportNode) in the main document and than append the child (AppendChild) to the specific node:

$myitems =
    @{ AssertName="Joe"; TestPass=$true },
    @{ AssertName="Sue"; TestPass=$false },
    @{ AssertName="Cat"; TestPass=$true }

$Main = [xml]'<?xml version="1.0"?><testsuites></testsuites>'

foreach ($item in $myitems) {
    $String = '<testsuite errors="0" failures="0" id="0" name="' + $item.AssertName + '" tests="1"><testcase classname="some.class.name" name="Test1" time="123.345000"/></testsuite>'
    $Xml = [xml]$String
    $Node = $Main.ImportNode($Xml.testsuite, $True)
    $Null = $Main.SelectSingleNode('testsuites').AppendChild($Node)
}
[System.Xml.Linq.XDocument]::Parse($Main.OuterXml).ToString()

<testsuites>
  <testsuite errors="0" failures="0" id="0" name="Joe" tests="1">
    <testcase classname="some.class.name" name="Test1" time="123.345000" />
  </testsuite>
  <testsuite errors="0" failures="0" id="0" name="Sue" tests="1">
    <testcase classname="some.class.name" name="Test1" time="123.345000" />
  </testsuite>
  <testsuite errors="0" failures="0" id="0" name="Cat" tests="1">
    <testcase classname="some.class.name" name="Test1" time="123.345000" />
  </testsuite>
</testsuites>
iRon
  • 20,463
  • 10
  • 53
  • 79