Public/Out-HashTableToXml.ps1

Function Out-HashTableToXml {

    <#
    .SYNOPSIS
        Converts a nested Hashtable into an xml string (and optionally writes it to a file).
 
    .DESCRIPTION
        Apteco PS Modules - Convert Hashtable to XML
 
        The reverse direction of Convert-XMLtoPSObject: turns a Hashtable (nested Hashtables/arrays allowed)
        into an xml document under a given root element. Supports namespaces on the root element.
 
        Originally from https://gallery.technet.microsoft.com/scriptcenter/Export-Hashtable-to-xml-in-122fda31/view/Discussions#content,
        with namespace support on the root node added on top.
 
    .PARAMETER Root
        Name of the xml root element, e.g. "Config" or "ns:Config" when using -Namespaces.
 
    .PARAMETER InputObject
        The Hashtable to convert. Nested Hashtables become nested elements, arrays repeat the element.
 
    .PARAMETER Path
        Optional file path to also save the resulting xml to.
 
    .PARAMETER Namespaces
        Optional Hashtable of prefix -> uri namespace declarations, e.g. @{ ns = "http://example.com/ns" }.
 
    .EXAMPLE
        @{ Name = "Test"; Values = @{ A = 1; B = 2 } } | Out-HashTableToXml -Root "Config"
 
    .EXAMPLE
        Out-HashTableToXml -Root "ns:Config" -InputObject $ht -Namespaces @{ ns = "http://example.com/ns" } -Path "config.xml"
 
    .INPUTS
        System.Collections.Hashtable
 
    .OUTPUTS
        String (the xml)
 
    .NOTES
        Author: florian.von.bracht@apteco.de
        Rescued from a local-only script
 
    #>


    [CmdletBinding(SupportsShouldProcess=$false)]
    Param(
         [ValidateNotNullOrEmpty()]
         [Parameter(Mandatory=$true)][System.String]$Root
        ,[Parameter(ValueFromPipeline=$true, Position=0)][System.Collections.Hashtable]$InputObject
        ,[Parameter(Mandatory=$false)][ValidateScript({ Test-Path $_ -IsValid })][System.String]$Path = ""
        ,[Parameter(Mandatory=$false)][System.Collections.Hashtable]$Namespaces = @{}
    )

    Begin {

        $ScriptBlock = {
            Param($Elem, $ElemRoot)
            If ( $Elem.Value -is [Array] ) {
                $Elem.Value | ForEach-Object {
                    $p = [System.Collections.DictionaryEntry]@{ "Key" = $Elem.Key; "Value" = $_ }
                    $ScriptBlock.Invoke($p, $ElemRoot)
                }
            } ElseIf ( $Elem.Value -is [System.Collections.Hashtable] ) {
                $childNode = $ElemRoot.AppendChild($Doc.CreateNode([System.Xml.XmlNodeType]::Element, $Elem.Key, $Null))
                $Elem.Value.GetEnumerator() | ForEach-Object {
                    $ScriptBlock.Invoke( @($_, $childNode) )
                }
            } Else {
                $Element = $Doc.CreateElement($Elem.Key)
                $p = If ( $Elem.Value -is [Array] ) {
                    $Elem.Value -join ','
                } Else {
                    $Elem.Value | Out-String
                }
                If ( $p -match '\S' ) { $Element.InnerText = $p.Trim() }
                $ElemRoot.AppendChild($Element) | Out-Null
            }
        }

    }

    Process {

        # Create empty xml document
        $Doc = New-Object System.XML.XMLDocument

        # Register namespaces
        $nsm = New-Object System.Xml.XmlNamespaceManager($Doc.NameTable)
        $Namespaces.Keys | ForEach-Object {
            $prefix = $_
            $uri = $Namespaces[$prefix]
            $nsm.AddNamespace($prefix, $uri)
        }

        # Resolve the root element's namespace, if any
        If ( $Root -like "*:*" ) {
            $rootPrefix = ( $Root -split ":" )[0]
        }
        $namespace = If ( $Namespaces.Count -gt 0 ) { $nsm.LookupNamespace($rootPrefix) } Else { $null }

        # Create root node
        $rootNode = $Doc.CreateNode([System.Xml.XmlNodeType]::Element, $Root, $namespace)
        $Doc.AppendChild($rootNode) | Out-Null

        # Add xml declaration
        $XD = $Doc.CreateXmlDeclaration("1.0", "UTF-8", "yes")
        $Doc.InsertBefore($XD, $Doc.DocumentElement) | Out-Null

        # Add subnodes recursively
        $InputObject.GetEnumerator() | ForEach-Object {
            $ScriptBlock.Invoke( @($_, $Doc.DocumentElement) )
        }

        # Save to file, if a path was provided
        If ( $Path -ne "" ) {
            $Doc.Save($Path)
        }

        return $Doc.OuterXml

    }

}