xmlpowershellplistpretty-print

Replacing illegal characters in XML / plist file and keeping the formatting


Embarcadero IDE uses a template plist file to be used for building iOS applications. The template file is a simple XML file, however it contains illegal placeholder symbols which Embarcadero resolves during deployment. (e.g. \<%VersionInfoPListKeys%\>)

Also due to limitations in the IDE, i have to manually edit the template file, to add custom entries which reference the bundle id. Since Embarcadero does not support xcode's variables inside plist files such as $(PRODUCT_BUNDLE_IDENTIFIER), I have to manually edit the file using a powershell script. However, due to those placeholder symbols, powershell cannot convert the contents of the file to an XML object.

Therefore I tried replacing those % characters with valid symbols, and before saving the file, replace the original symbols back.

I tried the following: ($plistPath and $bundleId are launch parameters)

Loading in the plist:

$plist_raw = ([System.IO.File]::ReadAllText($plistPath)) -replace "<%", "<_" -replace "%>", "_/>"
$plist = [xml]::new()
$plist.PreserveWhitespace = $true
$plist.LoadXml($plist_raw)

Adding / replacing an array node to the plist:

$arrayKey = "some_key"
$keyNode = (Select-Xml -Xml $plist -XPath "//dict/key" | Where-Object { $_.Node.InnerText -eq $arrayKey } | Select-Object -First 1).Node
if ($keyNode) {
    $plist.plist.dict.RemoveChild($keyNode.NextSibling)
    $plist.plist.dict.RemoveChild($keyNode)
}

$arrayNode = $plist.CreateElement('array')
$arrayKeyNode = $plist.CreateElement('key')
$arrayKeyNode.InnerText = $arrayKey
$plist.plist.dict.AppendChild($arrayKeyNode)
$plist.plist.dict.AppendChild($arrayNode)

$value1 = "$bundleId.foo"
$value2 = "$bundleId.bar"

foreach ($item in @($value1, $value2)) {
    $newItem = $plist.CreateElement('string')
    $newItem.InnerText = $item
    $arrayNode.AppendChild($newItem)
    Write-Host "Identifier '$item' has been added to the array '$arrayKey'."
} 

saving the plist:

($plist.OuterXml -replace "<_", "<%" -replace "_/>", "%>" -replace "_ />", "%>") | Set-Content $plistPath

The problem is, if I do this, the array node is appended to the file as a single line and has no formatting. It also fails to delete the existing array node.

Input file: "info.plist.TemplateiOS.xml"

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"[]>
<plist version="1.0">
<dict>
<%VersionInfoPListKeys%>
<%ExtraInfoPListKeys%>
<%StoryboardInfoPListKey%>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>processing</string>
</array>
</dict>
</plist>

The output I expect:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"[]>
<plist version="1.0">
<dict>
<%VersionInfoPListKeys%>
<%ExtraInfoPListKeys%>
<%StoryboardInfoPListKey%>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>processing</string>
</array>
<key>some_key</key>
<array>
  <string>some_bundleId.foo</string>
  <string>some_bundleId.bar</string>
</array>
</dict>
</plist>

The output I get:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"[]>
<plist version="1.0">
<dict>
<%VersionInfoPListKeys%>
<%ExtraInfoPListKeys%>
<%StoryboardInfoPListKey%>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>processing</string>
</array>
<array>
  <string>some_bundleId.foo</string>
  <string>some_bundleId.bar</string>
</array>
<key>some_key</key><array><string>some_bundleId.foo</string><string>some_bundleId.bar</string></array></dict>
</plist>

notice it only deleted the key and not the array, then put the new nodes below it.

If I use Get-Content to load the plist, it properly appends / deletes the nodes, but then the PreserveWhitespace property has no effect and the output is written into a single line.

To clarify, the script above is supposed to add a key "some_key" and an array with 2 strings. if that key is already present, it should overwrite it. The second output shown below is when that key was already present and it failed to delete the array. it does work when used Get-Content so it must be a side effect of the lacking formatting.


Solution

  • I suggest not trying to preserve the original pretty-printing, but modifying the document as desired and then pretty-printing the modified document anew, as a whole:

    # Creates a pretty-printed string representation of your [xml] document, $plist.
    # .Dispose() calls omitted for brevity.
    $xmlWriter = [System.Xml.XmlTextWriter] ($stringWriter = [System.IO.StringWriter]::new())
    $xmlWriter.Formatting = 'indented'; $xmlWriter.Indentation = 2
    $plist.WriteContentTo($xmlWriter)
    
    $prettyPrintedXml =
      $stringWriter.ToString()
    

    The above uses a System.Xml.XmlTextWriter instance that targets a System.IO.TextWriter instance to create a pretty-printed string representation of the [xml] (System.Xml.XmlDocument) instance stored in $plist.


    To put it all together in the context of your code:

    $plist_raw = (Get-Content $plistPath -Raw) -replace '<%', '<_' -replace '%>', '_/>'
    
    $plist = [xml]::new()
    $plist.LoadXml($plist_raw)
    
    $arrayKey = 'some_key'
    $keyNode = $plist.SelectSingleNode("//dict/key[.='$arrayKey']")
    
    $null = if ($keyNode) {
        $plist.plist.dict.RemoveChild($keyNode.NextSibling)
        $plist.plist.dict.RemoveChild($keyNode)
    }
    
    $arrayNode = $plist.CreateElement('array')
    $arrayKeyNode = $plist.CreateElement('key')
    $arrayKeyNode.InnerText = $arrayKey
    $null = $plist.plist.dict.AppendChild($arrayKeyNode)
    $null = $plist.plist.dict.AppendChild($arrayNode)
    
    $value1 = "$bundleId.foo"
    $value2 = "$bundleId.bar"
    
    foreach ($item in @($value1, $value2)) {
        $newItem = $plist.CreateElement('string')
        $newItem.InnerText = $item
        $null = $arrayNode.AppendChild($newItem)
        Write-Host "Identifier '$item' has been added to the array '$arrayKey'."
    }
    
    # Create a pretty-printed string representation of the modified document.
    $xmlWriter = [System.Xml.XmlTextWriter] ($stringWriter = [System.IO.StringWriter]::new())
    $xmlWriter.Formatting = 'indented'; $xmlWriter.Indentation = 2
    $plist.WriteContentTo($xmlWriter)
    $prettyPrintedXml = $stringWriter.ToString()
    $stringWriter.Dispose()
    $xmlWriter.Dispose()
    
    # Restore the templates and save back to the input file.
    $prettyPrintedXml -replace '<_', '<%' -replace '_ ?/>', '%>' |
      Set-Content $plistPath
    

    Note: