Search code examples
xmlpowershelldecodeencodexmlupdate

Power shell XML node update automatically encode <> charactors


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)

Solution

  • 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>