Reputation: 39
I am using PowerShell to load an XML File and I would like to add new entries into the selected nodes.
Here is the XML Document:
<?xml version="1.0"?>
<MDMPolicyMappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.microsoft.com/MdmMigrationAnalysisTool">
<Computer>
<PolicyMap xsi:type="OptionalPolicyMap">
<Name>Accounts: Administrator account status</Name>
<CspUri>./Device/Vendor/MSFT/Policy/Config/LocalPoliciesSecurityOptions/Accounts_EnableAdministratorAccountStatus</CspUri>
<CspName>Policy</CspName>
<Version>16299</Version>
</PolicyMap>
</Computer>
<User>
<PolicyMap xsi:type="AdmxPolicyMap">
<Name>Control Panel/Printers/Point and Print Restrictions</Name>
<CspUri>./User/Vendor/MSFT/Policy/Config/Printers/PointAndPrintRestrictions_User</CspUri>
<CspName>Policy</CspName>
<Version>15063</Version>
</PolicyMap>
</User>
</MDMPolicyMappings>
I use this Powershell command:
[xml]$XmlMmat = GC "C:\MDMPolicyMapping.xml"
But I am not sure how I can add new records to the Computer and User nodes.
For example I would like to add this to Computer:
<PolicyMap xsi:type="OptionalPolicyMap">
<Name>Test Policy</Name>
<CspUri>./Device/Vendor/MSFT/Policy/Config/Test</CspUri>
<CspName>Policy</CspName>
<Version>0000</Version>
</PolicyMap>
Does anyone know what I would need to do to get this block of XML added into the main file?
Thanks.
Upvotes: 2
Views: 5197
Reputation: 440297
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.
# This *automatically* creates a pretty-printed file.
# $xmlMmat.Save('C:\MDMPolicyMapping.xml') # !! Always use a FULL path.
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:
[xml]
does by default (that is, any original pretty-printing is lost).[xml]
and [System.Xml.Linq.XDocument]
do by default, when given the path of a file to save to.[System.Xml.Linq.XDocument]
makes it easy to create an in-memory string representation that is pretty-printed.[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 andXmlDocument.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.
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).
Upvotes: 7
Reputation: 2537
Here is a somewhat different approach to this problem:
$xml = New-Object xml
$xml.PreserveWhitespace = $true
$xml.Load("C:\Users\fpm\Desktop\MDMPolicyMapping.xml")
$txtFrag = @'
<dummy xmlns="http://www.microsoft.com/MdmMigrationAnalysisTool"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<PolicyMap xsi:type="OptionalPolicyMap">
<Name>Test Policy</Name>
<CspUri>./Device/Vendor/MSFT/Policy/Config/Test</CspUri>
<CspName>Policy</CspName>
<Version>0000</Version>
</PolicyMap>
</dummy>
'@
$xmlFrag=$xml.CreateDocumentFragment()
$xmlFrag.InnerXml=$txtFrag
$xml.MDMPolicyMappings.Computer.AppendChild($xmlFrag.dummy.PolicyMap)
$xml.Save("C:\Users\fpm\Desktop\output.xml")
Write-Output $xml.OuterXml
Upvotes: 3