Search code examples
xmlxsltformattingplistxmllint

Automatically Format an OS X/macOS XML Property List's Indentation and Self-Closing Tag Styles


     I'd like to automatically format one or more OS X/macOS XML property list files to use formatting like one would get by running 'xmllint --format' on them, except that:

  • I want keys' values to similarly be indented by one level (two spaces) underneath their parent keys.
  • I don't want keys with '<data>' values to have those get split across multiple lines.

I also want all self-closing tags to have spaces just before their closing slashes. What XSLT (edit: or other) transformation would I use to achieve this?


     As requested, here is a minimal working example (adapted slightly from this piece of Apple documentation:)

  • Before:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Year Of Birth</key>
    <integer>1965</integer>
    <key>Pets&apos; Names</key>
    <array/>
    <key>Picture</key>
    <data>
        PEKBpYGlmYFCPA==
    </data>
    <key>City of Birth</key>
    <string>Springfield</string>
    <key>Name</key>
    <string>John Doe</string>
    <key>Kids&apos; Names</key>
    <array>
        <string>John</string>
        <string>Kyra</string>
    </array>
    <key>Pangram</key>
    <data>
    VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gIA
    </data>
    <key>FollowingKeyDataLengthIsInsane</key>
    <true/>
    <key>&apos;Lorem Ipsum&apos; Sample Text</key>
    <data>
    TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwg
    c2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWdu
    YSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0
    aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1
    YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2
    ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRl
    dXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1
    aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpDdXJhYml0dXIg
    cHJldGl1bSB0aW5jaWR1bnQgbGFjdXMuIE51bGxhIGdyYXZpZGEgb3JjaSBhIG9kaW8uIE51bGxh
    bSB2YXJpdXMsIHR1cnBpcyBldCBjb21tb2RvIHBoYXJldHJhLCBlc3QgZXJvcyBiaWJlbmR1bSBl
    bGl0LCBuZWMgbHVjdHVzIG1hZ25hIGZlbGlzIHNvbGxpY2l0dWRpbiBtYXVyaXMuIEludGVnZXIg
    aW4gbWF1cmlzIGV1IG5pYmggZXVpc21vZCBncmF2aWRhLiBEdWlzIGFjIHRlbGx1cyBldCByaXN1
    cyB2dWxwdXRhdGUgdmVoaWN1bGEuIERvbmVjIGxvYm9ydGlzIHJpc3VzIGEgZWxpdC4gRXRpYW0g
    dGVtcG9yLiBVdCB1bGxhbWNvcnBlciwgbGlndWxhIGV1IHRlbXBvciBjb25ndWUsIGVyb3MgZXN0
    IGV1aXNtb2QgdHVycGlzLCBpZCB0aW5jaWR1bnQgc2FwaWVuIHJpc3VzIGEgcXVhbS4gTWFlY2Vu
    YXMgZmVybWVudHVtIGNvbnNlcXVhdCBtaS4gRG9uZWMgZmVybWVudHVtLiBQZWxsZW50ZXNxdWUg
    bWFsZXN1YWRhIG51bGxhIGEgbWkuIER1aXMgc2FwaWVuIHNlbSwgYWxpcXVldCBuZWMsIGNvbW1v
    ZG8gZWdldCwgY29uc2VxdWF0IHF1aXMsIG5lcXVlLiBBbGlxdWFtIGZhdWNpYnVzLCBlbGl0IHV0
    IGRpY3R1bSBhbGlxdWV0LCBmZWxpcyBuaXNsIGFkaXBpc2Npbmcgc2FwaWVuLCBzZWQgbWFsZXN1
    YWRhIGRpYW0gbGFjdXMgZWdldCBlcmF0LiBDcmFzIG1vbGxpcyBzY2VsZXJpc3F1ZSBudW5jLiBO
    dWxsYW0gYXJjdS4gQWxpcXVhbSBjb25zZXF1YXQuIEN1cmFiaXR1ciBhdWd1ZSBsb3JlbSwgZGFw
    aWJ1cyBxdWlzLCBsYW9yZWV0IGV0LCBwcmV0aXVtIGFjLCBuaXNpLiBBZW5lYW4gbWFnbmEgbmlz
    bCwgbW9sbGlzIHF1aXMsIG1vbGVzdGllIGV1LCBmZXVnaWF0IGluLCBvcmNpLiBJbiBoYWMgaGFi
    aXRhc3NlIHBsYXRlYSBkaWN0dW1zdC4=
    </data>
</dict>
</plist>
  • (Edit: What xmllint outputs, given this example in a file named 'sample.plist:'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Year Of Birth</key>
    <integer>1965</integer>
    <key>Pets' Names</key>
    <array/>
    <key>Picture</key>
    <data>
        PEKBpYGlmYFCPA==
    </data>
    <key>City of Birth</key>
    <string>Springfield</string>
    <key>Name</key>
    <string>John Doe</string>
    <key>Kids' Names</key>
    <array>
      <string>John</string>
      <string>Kyra</string>
    </array>
    <key>Pangram</key>
    <data>
    VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gIA
    </data>
    <key>FollowingKeyDataLengthIsInsane</key>
    <true/>
    <key>'Lorem Ipsum' Sample Text</key>
    <data>
    TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwg
    c2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWdu
    YSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0
    aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1
    YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2
    ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRl
    dXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1
    aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpDdXJhYml0dXIg
    cHJldGl1bSB0aW5jaWR1bnQgbGFjdXMuIE51bGxhIGdyYXZpZGEgb3JjaSBhIG9kaW8uIE51bGxh
    bSB2YXJpdXMsIHR1cnBpcyBldCBjb21tb2RvIHBoYXJldHJhLCBlc3QgZXJvcyBiaWJlbmR1bSBl
    bGl0LCBuZWMgbHVjdHVzIG1hZ25hIGZlbGlzIHNvbGxpY2l0dWRpbiBtYXVyaXMuIEludGVnZXIg
    aW4gbWF1cmlzIGV1IG5pYmggZXVpc21vZCBncmF2aWRhLiBEdWlzIGFjIHRlbGx1cyBldCByaXN1
    cyB2dWxwdXRhdGUgdmVoaWN1bGEuIERvbmVjIGxvYm9ydGlzIHJpc3VzIGEgZWxpdC4gRXRpYW0g
    dGVtcG9yLiBVdCB1bGxhbWNvcnBlciwgbGlndWxhIGV1IHRlbXBvciBjb25ndWUsIGVyb3MgZXN0
    IGV1aXNtb2QgdHVycGlzLCBpZCB0aW5jaWR1bnQgc2FwaWVuIHJpc3VzIGEgcXVhbS4gTWFlY2Vu
    YXMgZmVybWVudHVtIGNvbnNlcXVhdCBtaS4gRG9uZWMgZmVybWVudHVtLiBQZWxsZW50ZXNxdWUg
    bWFsZXN1YWRhIG51bGxhIGEgbWkuIER1aXMgc2FwaWVuIHNlbSwgYWxpcXVldCBuZWMsIGNvbW1v
    ZG8gZWdldCwgY29uc2VxdWF0IHF1aXMsIG5lcXVlLiBBbGlxdWFtIGZhdWNpYnVzLCBlbGl0IHV0
    IGRpY3R1bSBhbGlxdWV0LCBmZWxpcyBuaXNsIGFkaXBpc2Npbmcgc2FwaWVuLCBzZWQgbWFsZXN1
    YWRhIGRpYW0gbGFjdXMgZWdldCBlcmF0LiBDcmFzIG1vbGxpcyBzY2VsZXJpc3F1ZSBudW5jLiBO
    dWxsYW0gYXJjdS4gQWxpcXVhbSBjb25zZXF1YXQuIEN1cmFiaXR1ciBhdWd1ZSBsb3JlbSwgZGFw
    aWJ1cyBxdWlzLCBsYW9yZWV0IGV0LCBwcmV0aXVtIGFjLCBuaXNpLiBBZW5lYW4gbWFnbmEgbmlz
    bCwgbW9sbGlzIHF1aXMsIG1vbGVzdGllIGV1LCBmZXVnaWF0IGluLCBvcmNpLiBJbiBoYWMgaGFi
    aXRhc3NlIHBsYXRlYSBkaWN0dW1zdC4=
    </data>
  </dict>
</plist>

This isn't what I want, but I've added it for completeness's sake.)

  • After:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Year Of Birth</key>
    <integer>1965</integer>
  <key>Pets&apos; Names</key>
    <array />
  <key>Picture</key>
    <data>
      PEKBpYGlmYFCPA==
    </data>
  <key>City of Birth</key>
    <string>Springfield</string>
  <key>Name</key>
    <string>John Doe</string>
  <key>Kids&apos; Names</key>
    <array>
      <string>John</string>
      <string>Kyra</string>
    </array>
  <key>Pangram</key>
    <data>
      VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4gIA
    </data>
  <key>FollowingKeyDataLengthIsInsane</key>
    <true />
  <key>&apos;Lorem Ipsum&apos; Sample Text</key>
    <data>
      TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwg
      c2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWdu
      YSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0
      aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1
      YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2
      ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRl
      dXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1
      aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpDdXJhYml0dXIg
      cHJldGl1bSB0aW5jaWR1bnQgbGFjdXMuIE51bGxhIGdyYXZpZGEgb3JjaSBhIG9kaW8uIE51bGxh
      bSB2YXJpdXMsIHR1cnBpcyBldCBjb21tb2RvIHBoYXJldHJhLCBlc3QgZXJvcyBiaWJlbmR1bSBl
      bGl0LCBuZWMgbHVjdHVzIG1hZ25hIGZlbGlzIHNvbGxpY2l0dWRpbiBtYXVyaXMuIEludGVnZXIg
      aW4gbWF1cmlzIGV1IG5pYmggZXVpc21vZCBncmF2aWRhLiBEdWlzIGFjIHRlbGx1cyBldCByaXN1
      cyB2dWxwdXRhdGUgdmVoaWN1bGEuIERvbmVjIGxvYm9ydGlzIHJpc3VzIGEgZWxpdC4gRXRpYW0g
      dGVtcG9yLiBVdCB1bGxhbWNvcnBlciwgbGlndWxhIGV1IHRlbXBvciBjb25ndWUsIGVyb3MgZXN0
      IGV1aXNtb2QgdHVycGlzLCBpZCB0aW5jaWR1bnQgc2FwaWVuIHJpc3VzIGEgcXVhbS4gTWFlY2Vu
      YXMgZmVybWVudHVtIGNvbnNlcXVhdCBtaS4gRG9uZWMgZmVybWVudHVtLiBQZWxsZW50ZXNxdWUg
      bWFsZXN1YWRhIG51bGxhIGEgbWkuIER1aXMgc2FwaWVuIHNlbSwgYWxpcXVldCBuZWMsIGNvbW1v
      ZG8gZWdldCwgY29uc2VxdWF0IHF1aXMsIG5lcXVlLiBBbGlxdWFtIGZhdWNpYnVzLCBlbGl0IHV0
      IGRpY3R1bSBhbGlxdWV0LCBmZWxpcyBuaXNsIGFkaXBpc2Npbmcgc2FwaWVuLCBzZWQgbWFsZXN1
      YWRhIGRpYW0gbGFjdXMgZWdldCBlcmF0LiBDcmFzIG1vbGxpcyBzY2VsZXJpc3F1ZSBudW5jLiBO
      dWxsYW0gYXJjdS4gQWxpcXVhbSBjb25zZXF1YXQuIEN1cmFiaXR1ciBhdWd1ZSBsb3JlbSwgZGFw
      aWJ1cyBxdWlzLCBsYW9yZWV0IGV0LCBwcmV0aXVtIGFjLCBuaXNpLiBBZW5lYW4gbWFnbmEgbmlz
      bCwgbW9sbGlzIHF1aXMsIG1vbGVzdGllIGV1LCBmZXVnaWF0IGluLCBvcmNpLiBJbiBoYWMgaGFi
      aXRhc3NlIHBsYXRlYSBkaWN0dW1zdC4=
    </data>
</dict>
</plist>

Solution

  • FWIW, here's how you can indent the output yourself:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="no"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:param name="indent-unit" select="'&#9;'"/>
    
    <xsl:template match="*">
        <xsl:param name="indent" select="'&#10;'"/>
        <xsl:value-of select="$indent"/>
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:apply-templates>
                <xsl:with-param name="indent" select="concat($indent, $indent-unit)"/>
            </xsl:apply-templates>
        </xsl:copy>
        <xsl:if test="not(following-sibling::*)">
            <xsl:value-of select="substring($indent, 1, string-length($indent) - string-length($indent-unit))"/>
        </xsl:if>
    </xsl:template>
    
    <xsl:template match="*[preceding-sibling::*[1][self::key]]">
        <xsl:param name="indent"/>
        <xsl:value-of select="concat($indent, $indent-unit)"/>
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:apply-templates>
                <xsl:with-param name="indent" select="concat($indent, $indent-unit, $indent-unit)"/>
            </xsl:apply-templates>
        </xsl:copy>
        <xsl:if test="not(following-sibling::*)">
            <xsl:value-of select="substring($indent, 1, string-length($indent) - string-length($indent-unit))"/>
        </xsl:if>
    </xsl:template>
    
    </xsl:stylesheet>
    

    Demo: https://xsltfiddle.liberty-development.net/pNmC4J6