Powershell – Creating new entries into an XML File

What the [xml] cast constructs is a System.Xml.XmlDocument instance.

You can use its methods to manipulate the XML DOM, and you can use PowerShell’s adaptation of that DOM to easily drill down to the parent element of interest.

In your case, the involvement of XML namespaces makes the insertion of the new elements challenging.

Update: The new element is now constructed via an auxiliary document fragment (constructed with .CreateDocumentFragment()) rather than via an aux. second document, as first shown in fpmurphy’s helpful answer.

# Read the file into an XML DOM ([System.Xml.XmlDocument]; type accelerator [xml]).
# NOTE: You should use Get-Content to read an XML file ONLY if
#       you know the document's character encoding (see bottom section).
#       The robust alternative, where the XML declaration is analyzed
#       for the encoding actually used, is:
#        [xml] $xmlMmat = [xml]::new()
#        $xmlMat.Load('C:\MDMPolicyMapping.xml') # !! Always use a FULL path.
[xml] $xmlMmat = Get-Content -Encoding utf8 -Raw C:\MDMPolicyMapping.xml

# Create the element to insert.
# An aux. XML document fragment *with the same namespace declarations* as the target
# document must be used, and these are attached to a dummy *document node*
# enclosing the actual element, so that the namespace declarations don't become
# part of the element itself.
$xmlFragment = $xmlMmat.CreateDocumentFragment()
$xmlFragment.InnerXml =
@'
<aux xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.microsoft.com/MdmMigrationAnalysisTool">
  <PolicyMap xsi:type="OptionalPolicyMap">
    <Name>Test Policy</Name>
    <CspUri>./Device/Vendor/MSFT/Policy/Config/Test</CspUri>
    <CspName>Policy</CspName>
    <Version>0000</Version>
  </PolicyMap>
</aux>
'@

# Use dot notation to drill down to the <Computer> element and
# append the new element as a new child node.
$null = $xmlMmat.MDMPolicyMappings.Computer.AppendChild(
          $xmlFragment.aux.PolicyMap
        )

# For illustration, pretty-print the resulting XML to the screen.
# Note: The LINQ-related [Xml.Linq.XDocument] type offers pretty-printing by 
#       default, so casting the XML text to it and calling .ToString() on
#       the resulting instance pretty-prints automatically, but note that
# while this is convenient, it is *inefficient*, because the whole document
#       is parsed again.
([System.Xml.Linq.XDocument] $xmlMmat.OuterXml).ToString()

# Now you can save the modified DOM to a file, e.g., to save back to the
# original one, but WITHOUT PRETTY-PRINTING:
#
#    $xmlMmat.Save('C:\MDMPolicyMapping.xml') # !! Always use a FULL path.

# To save PRETTY-PRINTED XML, more work is needed, using 
# a [System.Xml.XmlTextWriter] instance:
#
#   # !! Always specify the output file as a *full path*.
#   $xmlWriter = [System.Xml.XmlTextWriter] [System.IO.StreamWriter] 'C:\MDMPolicyMapping.xml'
#   $xmlWriter.Formatting = 'indented'; $xmlWriter.Indentation = 2
#   $xmlMMat.WriteContentTo($xmlWriter)
#   $xmlWriter.Dispose()

Note: If you don’t mind the extra overhead of parsing the XML again, into a System.Xml.Linq.XDocument instance, saving to a pretty-printed file is as easy as:

# Inefficient, but convenient way to save an [xml] instance pretty-printed.
# !! Always use a *full path*:
([System.Xml.Linq.XDocument] $xmlMmat.OuterXml).Save('C:\MDMPolicyMapping.xml')

Re XML pretty-printing:

Note that you can opt to preserve insignificant whitespace (as used in pretty-printing) on loading an XML file, by setting an [xml] instance’s .PreserveWhitepace property to $true before calling .Load():

  • If the input file was pretty-printed to begin with and you make no structural changes, .Save() will preserve the pretty-printed format of the document.

  • If you do make structural changes, the only way to preserve the pretty-printed format is to manually match the expected formatting whitespace so that it fits in with the existing pretty-printing.

Overall, the more robust approach is to:

  • Ignore insignificant whitespace on loading, which [xml] does by default (that is, any original pretty-printing is lost).
  • Explicitly pretty-print on saving, if needed (which [xml] doesn’t make easy, unfortunately – unlike [System.Xml.Linq.XDocument]).

Optional reading: The often-used, but flawed [xml] (Get-Content ...) shortcut for reading XML from a file:

Tomalak suggests phrasing the caveat regarding this shortcut as follows:

You should use NEVER use Get-Content to read an XML file unless the file is broken and XmlDocument.Load() fails.

The reason is that a statement such as [xml] $xmlDoc = Get-Content -Raw file.xml[1] (or, without type-constraining the variable, $xmlDoc = [xml] (Get-Content -Raw some.xml)) may misinterpret the file’s character encoding:

  • The default character encoding for XML files is UTF-8; that is, in the absence of a BOM (and without an encoding attribute in the XML declaration, see below) UTF-8 should be assumed.

    • However, in Windows PowerShell Get-Content assumes ANSI encoding (the system’s active ANSI code page as determined by the system locale (language for non-Unicode programs)) in the absence of a BOM. Fortunately, PowerShell (Core) v6+ now consistently defaults to (BOM-less) UTF-8.
  • While you can address this problem with -Encoding ut8 in Windows PowerShell, this is not sufficient, because an XML document is free to specify its actual encoding as part of the document’s content, namely via the XML declaration’s encoding attribute; e.g.:
    <?xml version="1.0" encoding="ISO-8859-1"?>

    • Since Get-Content decides what encoding to use solely based on the absence / presence of a BOM, it does not honor the declared encoding.

    • By contrast, the [xml] (System.Xml.XmlDocument) type’s .Load() method does, which is why it is the only robust way to read XML documents.[2]

    • Similarly, you should use the .Save() method for properly saving an [xml] instance to a file.

It is unfortunate that the robust method, compared to the Get-Content shortcut is (a) more verbose and less fluid (you need to construct an [xml] instance explicitly and then call .Load() on it) and (b) treacherous (due to .NET APIs typically seeing a different working dir. than PowerShell, relative file paths malfunction):

# The ROBUST WAY to read an XML file:

# Construct an empty System.Xml.XmlDocument instance.
$xmlDoc = [xml]::new()

# Determine the input file's *full path*, as only that
# guarantees that the .Load() call below finds the file.
$fullName = Convert-Path ./file.xml

# Load from file and parse into an XML DOM.
$xmlDoc.Load($fullName)

You can shorten to this idiom, but it is still awkward:

($xmlDoc = [xml]::new()).Load((Convert-Path file.xml))

Potential future improvements:

  • If Get-Content itself were to be made XML-aware (so as to honor an encoding specified in the XML declaration), the shortcut would work robustly.

  • Alternatively, new syntactic sugar could be introduced to support casting a System.IO.FileInfo instance to [xml], such as returned by Get-Item and Get-ChildItem:

    # WISHFUL THINKING - cast a FileInfo instance to [xml]
    [xml] $xmlDoc = Get-Item ./file.xml
    

[1] -Raw isn’t strictly necessary, but greatly speeds up the operation.

[2] More generally, any proper XML parser should handle an encoding specified via the XML declaration correctly; for instance, The [System.Xml.Linq.XDocument] type’s .Load works too, but, unlike [System.Xml.XmlDocument], this type isn’t well integrated with PowerShell (no access to elements and attributes by convenient dot notation).

Leave a Comment