XML Parsing in OS X Terminal for MobileConfig file

2k views Asked by At

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>[email protected]</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 ?

2

There are 2 answers

6
Wintermute On BEST ANSWER

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:

  • //dict matches any dict node in the document,
  • //dict/key matches any key node that is the child of a dict node,
  • //dict/key[text() = "Password"] matches any key note that is the child of a dict node and contains the text Password,
  • //dict/key[text() = "Password"]/following-sibling matches any following sibling node of such a key node, which is to say any node that is a child of the same parent and comes after the key node in the XML,
  • //dict/key[text() = "Password"]/following-sibling::string matches any string node that is such a following sibling node, and
  • //dict/key[text() = "Password"]/following-sibling::string[1] matches any node that is the first following sibling string node of such a key node.

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).

1
Arjun Mathew Dan On

You can do it like this

sed -r "s#(<string>)SSID_STR(</string>)#\1AMD\2#g" File

For inplace substitution:

sed -i -r "s#(<string>)SSID_STR(</string>)#\1AMD\2#g" File

(, ) are used for grouping. \1 and \2 stands for the first and second such groups. Replace AMD with your actual content. similarly, u can do for Password.