iosxmlplistmdmapple-configurator

XML Parsing in OS X Terminal for MobileConfig file


I am working on generating (actually editing) a mobileconfig file (aka iOS profile, XML) via bash script.

The script fetch data from a MS Database and has now to inject/replace this data in my mobileconfig file (XML).

The XML file has the following structure:

<?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>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>Host</key>
            <string>outlook.office365.com</string>
            <key>MailNumberOfPastDaysToSync</key>
            <integer>7</integer>
            <key>Password</key>
            <string>ActiveSyncPassword</string>
            <key>PayloadDescription</key>
            <string>Configures an Exchange account</string>
            <key>PayloadDisplayName</key>
            <string>Exchange ActiveSync</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
            <key>SSL</key>
            <true/>
            <key>UserName</key>
            <string>xxxxxxx@xxx.com</string>
            <key>disableMailRecentsSyncing</key>
            <false/>
        </dict>
        <dict>
            <key>AutoJoin</key>
            <true/>
            <key>EncryptionType</key>
            <string>WPA</string>
            <key>HIDDEN_NETWORK</key>
            <true/>
            <key>IsHotspot</key>
            <false/>
            <key>Password</key>
            <string>WEPWPAWPSPEAPTLS</string>
            <key>PayloadType</key>
            <string>com.apple.wifi.managed</string>
            <key>PayloadVersion</key>
            <real>1</real>
            <key>ProxyType</key>
            <string>None</string>
            <key>SSID_STR</key>
            <string>SSID</string>
        </dict>
        <dict>

I would like to replace the WiFi Password but also ActiveSync "Password" fields between the < string> < /string> using any native (xmllint, sed) or non-native tool.

Can anyone please help ?


Solution

  • Editing structured data (such as XML) with plain-text tools invariably ends in misery when the file format changes in ways that nobody expects to make a difference (such as inserting benign whitespace). Instead, use a tool that parses XML properly and works on the tree, such as xmlstarlet.

    The general form for this is

    xmlstarlet ed -u xpath -v value filename.xml
    

    Where xpath is an XPath expression that identifies the node you want to update, and value is the new value you want to give it. The magic is in constructing an XPath expression that uniquely and reliably identifies the node you want to update. The MobileConfig XML format makes this somewhat harder than usual; after discussion in the comments we ended up with

    xmlstarlet ed -u '//dict[key[text() = "PayloadDisplayName"]/following-sibling::string[1] = "Exchange ActiveSync"]/key[text() = "Password"]/following-sibling::string[1]' -v 'abc123' filename.xml
    

    The core of this is the XPath expression

    //dict[key[text() = "PayloadDisplayName"]/following-sibling::string[1] = "Exchange ActiveSync"]/key[text() = "Password"]/following-sibling::string[1]
    

    ..which requires some explanation. We use the following features:

    We've already used a condition in //dict/key[text() = "Password"]; in order to find the dict node whose password entry is to be changed, we need more of that. The dict node we want to find is identified by

    //dict[key[text() = "PayloadDisplayName"]/following-sibling::string[1] = "Exchange ActiveSync"]
    

    That is a dict node that fulfills the condition

    key[text() = "PayloadDisplayName"]/following-sibling::string[1] = "Exchange ActiveSync"
    

    The XPath expressions in this condition are all relative to the dict node that's being tested, so

    key[text() = "PayloadDisplayName"]
    

    refers to a key subnode of that dict node that contains the text PayloadDisplayName, and

    key[text() = "PayloadDisplayName"]/following-sibling::string[1] = "Exchange ActiveSync"
    

    is true if the text in the string node that follows the key node that contains the text PayloadDisplayName is Exchange ActiveSync. So we chuck that into the simplified expression I explained above and get the full filter.

    I feel compelled to point out that the structure of this XML file makes the whole thing more difficult than necessary or usual. Sanely structured XML can be handled with much simpler XPath expressions (most of the time).