Post

Parsing property lists (*.plist) with Python

Sometimes one needs to know state of a plist before changing settings. For example, when changing LSHandlers in LaunchServices.

We can see what’s inside the file using defaults read command:

1
2
3
4
5
6
7
8
9
10
11
$ defaults read ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist
{
    LSHandlers =     (
                {
            LSHandlerPreferredVersions =             {
                LSHandlerRoleAll = "-";
            };
            LSHandlerRoleAll = "us.zoom.xos";
            LSHandlerURLScheme = zoomphonecall;
        },
...

Now we could use plists, pure-python stdlib-only Python package to parse the binary plist file, but the output of defaults looks so close to JSON that we could just convert it with just a few changes.

Let’s make plist.py containing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python
import json
import os
import re
import subprocess
import sys


def parse(path):
    p = subprocess.run(['defaults', 'read', os.path.abspath(path)],
                       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if p.returncode:
        raise NotImplementedError(p.stderr.decode())
    o = p.stdout.decode()
    o = re.sub(r' = ([^"]+);\n', ' = "\\1",\n', o)
    o = re.sub(r'(\S+) =\s*', '"\\1": ', o)
    o = re.sub(r':\s+\(', ': [', o)
    o = re.sub(r';\n', ',\n', o)
    o = re.sub(r'\)(,?)\n', ']\\1\n', o)
    o = re.sub(r',(\s+[}\]])', '\\1', o)
    return json.loads(o)


if __name__ == '__main__':
    print(json.dumps(parse(sys.argv[1])))

Now we can run it:

1
2
$ python plist.py ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist
{"LSHandlers": [{"LSHandlerPreferredVersions":...

and pipe it to yq:

1
python plist.py ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist | yq -P

to produce YAML:

1
2
3
4
5
6
LSHandlers:
  - LSHandlerPreferredVersions:
      LSHandlerRoleAll: '-'
    LSHandlerRoleAll: us.zoom.xos
    LSHandlerURLScheme: zoomphonecall
...

We can also make it available everywhere:

1
2
chmod +x plist.py
ln -s $(pwd)/plist.py /usr/local/bin/plist

and use it:

1
plist path-to-my.plist

Enjoy!

This post is licensed under CC BY 4.0 by the author.