xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/mailcap.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1*cda5da8dSAndroid Build Coastguard Worker"""Mailcap file handling.  See RFC 1524."""
2*cda5da8dSAndroid Build Coastguard Worker
3*cda5da8dSAndroid Build Coastguard Workerimport os
4*cda5da8dSAndroid Build Coastguard Workerimport warnings
5*cda5da8dSAndroid Build Coastguard Workerimport re
6*cda5da8dSAndroid Build Coastguard Worker
7*cda5da8dSAndroid Build Coastguard Worker__all__ = ["getcaps","findmatch"]
8*cda5da8dSAndroid Build Coastguard Worker
9*cda5da8dSAndroid Build Coastguard Worker
10*cda5da8dSAndroid Build Coastguard Worker_DEPRECATION_MSG = ('The {name} module is deprecated and will be removed in '
11*cda5da8dSAndroid Build Coastguard Worker                    'Python {remove}. See the mimetypes module for an '
12*cda5da8dSAndroid Build Coastguard Worker                    'alternative.')
13*cda5da8dSAndroid Build Coastguard Workerwarnings._deprecated(__name__, _DEPRECATION_MSG, remove=(3, 13))
14*cda5da8dSAndroid Build Coastguard Worker
15*cda5da8dSAndroid Build Coastguard Worker
16*cda5da8dSAndroid Build Coastguard Workerdef lineno_sort_key(entry):
17*cda5da8dSAndroid Build Coastguard Worker    # Sort in ascending order, with unspecified entries at the end
18*cda5da8dSAndroid Build Coastguard Worker    if 'lineno' in entry:
19*cda5da8dSAndroid Build Coastguard Worker        return 0, entry['lineno']
20*cda5da8dSAndroid Build Coastguard Worker    else:
21*cda5da8dSAndroid Build Coastguard Worker        return 1, 0
22*cda5da8dSAndroid Build Coastguard Worker
23*cda5da8dSAndroid Build Coastguard Worker_find_unsafe = re.compile(r'[^\xa1-\U0010FFFF\w@+=:,./-]').search
24*cda5da8dSAndroid Build Coastguard Worker
25*cda5da8dSAndroid Build Coastguard Workerclass UnsafeMailcapInput(Warning):
26*cda5da8dSAndroid Build Coastguard Worker    """Warning raised when refusing unsafe input"""
27*cda5da8dSAndroid Build Coastguard Worker
28*cda5da8dSAndroid Build Coastguard Worker
29*cda5da8dSAndroid Build Coastguard Worker# Part 1: top-level interface.
30*cda5da8dSAndroid Build Coastguard Worker
31*cda5da8dSAndroid Build Coastguard Workerdef getcaps():
32*cda5da8dSAndroid Build Coastguard Worker    """Return a dictionary containing the mailcap database.
33*cda5da8dSAndroid Build Coastguard Worker
34*cda5da8dSAndroid Build Coastguard Worker    The dictionary maps a MIME type (in all lowercase, e.g. 'text/plain')
35*cda5da8dSAndroid Build Coastguard Worker    to a list of dictionaries corresponding to mailcap entries.  The list
36*cda5da8dSAndroid Build Coastguard Worker    collects all the entries for that MIME type from all available mailcap
37*cda5da8dSAndroid Build Coastguard Worker    files.  Each dictionary contains key-value pairs for that MIME type,
38*cda5da8dSAndroid Build Coastguard Worker    where the viewing command is stored with the key "view".
39*cda5da8dSAndroid Build Coastguard Worker
40*cda5da8dSAndroid Build Coastguard Worker    """
41*cda5da8dSAndroid Build Coastguard Worker    caps = {}
42*cda5da8dSAndroid Build Coastguard Worker    lineno = 0
43*cda5da8dSAndroid Build Coastguard Worker    for mailcap in listmailcapfiles():
44*cda5da8dSAndroid Build Coastguard Worker        try:
45*cda5da8dSAndroid Build Coastguard Worker            fp = open(mailcap, 'r')
46*cda5da8dSAndroid Build Coastguard Worker        except OSError:
47*cda5da8dSAndroid Build Coastguard Worker            continue
48*cda5da8dSAndroid Build Coastguard Worker        with fp:
49*cda5da8dSAndroid Build Coastguard Worker            morecaps, lineno = _readmailcapfile(fp, lineno)
50*cda5da8dSAndroid Build Coastguard Worker        for key, value in morecaps.items():
51*cda5da8dSAndroid Build Coastguard Worker            if not key in caps:
52*cda5da8dSAndroid Build Coastguard Worker                caps[key] = value
53*cda5da8dSAndroid Build Coastguard Worker            else:
54*cda5da8dSAndroid Build Coastguard Worker                caps[key] = caps[key] + value
55*cda5da8dSAndroid Build Coastguard Worker    return caps
56*cda5da8dSAndroid Build Coastguard Worker
57*cda5da8dSAndroid Build Coastguard Workerdef listmailcapfiles():
58*cda5da8dSAndroid Build Coastguard Worker    """Return a list of all mailcap files found on the system."""
59*cda5da8dSAndroid Build Coastguard Worker    # This is mostly a Unix thing, but we use the OS path separator anyway
60*cda5da8dSAndroid Build Coastguard Worker    if 'MAILCAPS' in os.environ:
61*cda5da8dSAndroid Build Coastguard Worker        pathstr = os.environ['MAILCAPS']
62*cda5da8dSAndroid Build Coastguard Worker        mailcaps = pathstr.split(os.pathsep)
63*cda5da8dSAndroid Build Coastguard Worker    else:
64*cda5da8dSAndroid Build Coastguard Worker        if 'HOME' in os.environ:
65*cda5da8dSAndroid Build Coastguard Worker            home = os.environ['HOME']
66*cda5da8dSAndroid Build Coastguard Worker        else:
67*cda5da8dSAndroid Build Coastguard Worker            # Don't bother with getpwuid()
68*cda5da8dSAndroid Build Coastguard Worker            home = '.' # Last resort
69*cda5da8dSAndroid Build Coastguard Worker        mailcaps = [home + '/.mailcap', '/etc/mailcap',
70*cda5da8dSAndroid Build Coastguard Worker                '/usr/etc/mailcap', '/usr/local/etc/mailcap']
71*cda5da8dSAndroid Build Coastguard Worker    return mailcaps
72*cda5da8dSAndroid Build Coastguard Worker
73*cda5da8dSAndroid Build Coastguard Worker
74*cda5da8dSAndroid Build Coastguard Worker# Part 2: the parser.
75*cda5da8dSAndroid Build Coastguard Workerdef readmailcapfile(fp):
76*cda5da8dSAndroid Build Coastguard Worker    """Read a mailcap file and return a dictionary keyed by MIME type."""
77*cda5da8dSAndroid Build Coastguard Worker    warnings.warn('readmailcapfile is deprecated, use getcaps instead',
78*cda5da8dSAndroid Build Coastguard Worker                  DeprecationWarning, 2)
79*cda5da8dSAndroid Build Coastguard Worker    caps, _ = _readmailcapfile(fp, None)
80*cda5da8dSAndroid Build Coastguard Worker    return caps
81*cda5da8dSAndroid Build Coastguard Worker
82*cda5da8dSAndroid Build Coastguard Worker
83*cda5da8dSAndroid Build Coastguard Workerdef _readmailcapfile(fp, lineno):
84*cda5da8dSAndroid Build Coastguard Worker    """Read a mailcap file and return a dictionary keyed by MIME type.
85*cda5da8dSAndroid Build Coastguard Worker
86*cda5da8dSAndroid Build Coastguard Worker    Each MIME type is mapped to an entry consisting of a list of
87*cda5da8dSAndroid Build Coastguard Worker    dictionaries; the list will contain more than one such dictionary
88*cda5da8dSAndroid Build Coastguard Worker    if a given MIME type appears more than once in the mailcap file.
89*cda5da8dSAndroid Build Coastguard Worker    Each dictionary contains key-value pairs for that MIME type, where
90*cda5da8dSAndroid Build Coastguard Worker    the viewing command is stored with the key "view".
91*cda5da8dSAndroid Build Coastguard Worker    """
92*cda5da8dSAndroid Build Coastguard Worker    caps = {}
93*cda5da8dSAndroid Build Coastguard Worker    while 1:
94*cda5da8dSAndroid Build Coastguard Worker        line = fp.readline()
95*cda5da8dSAndroid Build Coastguard Worker        if not line: break
96*cda5da8dSAndroid Build Coastguard Worker        # Ignore comments and blank lines
97*cda5da8dSAndroid Build Coastguard Worker        if line[0] == '#' or line.strip() == '':
98*cda5da8dSAndroid Build Coastguard Worker            continue
99*cda5da8dSAndroid Build Coastguard Worker        nextline = line
100*cda5da8dSAndroid Build Coastguard Worker        # Join continuation lines
101*cda5da8dSAndroid Build Coastguard Worker        while nextline[-2:] == '\\\n':
102*cda5da8dSAndroid Build Coastguard Worker            nextline = fp.readline()
103*cda5da8dSAndroid Build Coastguard Worker            if not nextline: nextline = '\n'
104*cda5da8dSAndroid Build Coastguard Worker            line = line[:-2] + nextline
105*cda5da8dSAndroid Build Coastguard Worker        # Parse the line
106*cda5da8dSAndroid Build Coastguard Worker        key, fields = parseline(line)
107*cda5da8dSAndroid Build Coastguard Worker        if not (key and fields):
108*cda5da8dSAndroid Build Coastguard Worker            continue
109*cda5da8dSAndroid Build Coastguard Worker        if lineno is not None:
110*cda5da8dSAndroid Build Coastguard Worker            fields['lineno'] = lineno
111*cda5da8dSAndroid Build Coastguard Worker            lineno += 1
112*cda5da8dSAndroid Build Coastguard Worker        # Normalize the key
113*cda5da8dSAndroid Build Coastguard Worker        types = key.split('/')
114*cda5da8dSAndroid Build Coastguard Worker        for j in range(len(types)):
115*cda5da8dSAndroid Build Coastguard Worker            types[j] = types[j].strip()
116*cda5da8dSAndroid Build Coastguard Worker        key = '/'.join(types).lower()
117*cda5da8dSAndroid Build Coastguard Worker        # Update the database
118*cda5da8dSAndroid Build Coastguard Worker        if key in caps:
119*cda5da8dSAndroid Build Coastguard Worker            caps[key].append(fields)
120*cda5da8dSAndroid Build Coastguard Worker        else:
121*cda5da8dSAndroid Build Coastguard Worker            caps[key] = [fields]
122*cda5da8dSAndroid Build Coastguard Worker    return caps, lineno
123*cda5da8dSAndroid Build Coastguard Worker
124*cda5da8dSAndroid Build Coastguard Workerdef parseline(line):
125*cda5da8dSAndroid Build Coastguard Worker    """Parse one entry in a mailcap file and return a dictionary.
126*cda5da8dSAndroid Build Coastguard Worker
127*cda5da8dSAndroid Build Coastguard Worker    The viewing command is stored as the value with the key "view",
128*cda5da8dSAndroid Build Coastguard Worker    and the rest of the fields produce key-value pairs in the dict.
129*cda5da8dSAndroid Build Coastguard Worker    """
130*cda5da8dSAndroid Build Coastguard Worker    fields = []
131*cda5da8dSAndroid Build Coastguard Worker    i, n = 0, len(line)
132*cda5da8dSAndroid Build Coastguard Worker    while i < n:
133*cda5da8dSAndroid Build Coastguard Worker        field, i = parsefield(line, i, n)
134*cda5da8dSAndroid Build Coastguard Worker        fields.append(field)
135*cda5da8dSAndroid Build Coastguard Worker        i = i+1 # Skip semicolon
136*cda5da8dSAndroid Build Coastguard Worker    if len(fields) < 2:
137*cda5da8dSAndroid Build Coastguard Worker        return None, None
138*cda5da8dSAndroid Build Coastguard Worker    key, view, rest = fields[0], fields[1], fields[2:]
139*cda5da8dSAndroid Build Coastguard Worker    fields = {'view': view}
140*cda5da8dSAndroid Build Coastguard Worker    for field in rest:
141*cda5da8dSAndroid Build Coastguard Worker        i = field.find('=')
142*cda5da8dSAndroid Build Coastguard Worker        if i < 0:
143*cda5da8dSAndroid Build Coastguard Worker            fkey = field
144*cda5da8dSAndroid Build Coastguard Worker            fvalue = ""
145*cda5da8dSAndroid Build Coastguard Worker        else:
146*cda5da8dSAndroid Build Coastguard Worker            fkey = field[:i].strip()
147*cda5da8dSAndroid Build Coastguard Worker            fvalue = field[i+1:].strip()
148*cda5da8dSAndroid Build Coastguard Worker        if fkey in fields:
149*cda5da8dSAndroid Build Coastguard Worker            # Ignore it
150*cda5da8dSAndroid Build Coastguard Worker            pass
151*cda5da8dSAndroid Build Coastguard Worker        else:
152*cda5da8dSAndroid Build Coastguard Worker            fields[fkey] = fvalue
153*cda5da8dSAndroid Build Coastguard Worker    return key, fields
154*cda5da8dSAndroid Build Coastguard Worker
155*cda5da8dSAndroid Build Coastguard Workerdef parsefield(line, i, n):
156*cda5da8dSAndroid Build Coastguard Worker    """Separate one key-value pair in a mailcap entry."""
157*cda5da8dSAndroid Build Coastguard Worker    start = i
158*cda5da8dSAndroid Build Coastguard Worker    while i < n:
159*cda5da8dSAndroid Build Coastguard Worker        c = line[i]
160*cda5da8dSAndroid Build Coastguard Worker        if c == ';':
161*cda5da8dSAndroid Build Coastguard Worker            break
162*cda5da8dSAndroid Build Coastguard Worker        elif c == '\\':
163*cda5da8dSAndroid Build Coastguard Worker            i = i+2
164*cda5da8dSAndroid Build Coastguard Worker        else:
165*cda5da8dSAndroid Build Coastguard Worker            i = i+1
166*cda5da8dSAndroid Build Coastguard Worker    return line[start:i].strip(), i
167*cda5da8dSAndroid Build Coastguard Worker
168*cda5da8dSAndroid Build Coastguard Worker
169*cda5da8dSAndroid Build Coastguard Worker# Part 3: using the database.
170*cda5da8dSAndroid Build Coastguard Worker
171*cda5da8dSAndroid Build Coastguard Workerdef findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
172*cda5da8dSAndroid Build Coastguard Worker    """Find a match for a mailcap entry.
173*cda5da8dSAndroid Build Coastguard Worker
174*cda5da8dSAndroid Build Coastguard Worker    Return a tuple containing the command line, and the mailcap entry
175*cda5da8dSAndroid Build Coastguard Worker    used; (None, None) if no match is found.  This may invoke the
176*cda5da8dSAndroid Build Coastguard Worker    'test' command of several matching entries before deciding which
177*cda5da8dSAndroid Build Coastguard Worker    entry to use.
178*cda5da8dSAndroid Build Coastguard Worker
179*cda5da8dSAndroid Build Coastguard Worker    """
180*cda5da8dSAndroid Build Coastguard Worker    if _find_unsafe(filename):
181*cda5da8dSAndroid Build Coastguard Worker        msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
182*cda5da8dSAndroid Build Coastguard Worker        warnings.warn(msg, UnsafeMailcapInput)
183*cda5da8dSAndroid Build Coastguard Worker        return None, None
184*cda5da8dSAndroid Build Coastguard Worker    entries = lookup(caps, MIMEtype, key)
185*cda5da8dSAndroid Build Coastguard Worker    # XXX This code should somehow check for the needsterminal flag.
186*cda5da8dSAndroid Build Coastguard Worker    for e in entries:
187*cda5da8dSAndroid Build Coastguard Worker        if 'test' in e:
188*cda5da8dSAndroid Build Coastguard Worker            test = subst(e['test'], filename, plist)
189*cda5da8dSAndroid Build Coastguard Worker            if test is None:
190*cda5da8dSAndroid Build Coastguard Worker                continue
191*cda5da8dSAndroid Build Coastguard Worker            if test and os.system(test) != 0:
192*cda5da8dSAndroid Build Coastguard Worker                continue
193*cda5da8dSAndroid Build Coastguard Worker        command = subst(e[key], MIMEtype, filename, plist)
194*cda5da8dSAndroid Build Coastguard Worker        if command is not None:
195*cda5da8dSAndroid Build Coastguard Worker            return command, e
196*cda5da8dSAndroid Build Coastguard Worker    return None, None
197*cda5da8dSAndroid Build Coastguard Worker
198*cda5da8dSAndroid Build Coastguard Workerdef lookup(caps, MIMEtype, key=None):
199*cda5da8dSAndroid Build Coastguard Worker    entries = []
200*cda5da8dSAndroid Build Coastguard Worker    if MIMEtype in caps:
201*cda5da8dSAndroid Build Coastguard Worker        entries = entries + caps[MIMEtype]
202*cda5da8dSAndroid Build Coastguard Worker    MIMEtypes = MIMEtype.split('/')
203*cda5da8dSAndroid Build Coastguard Worker    MIMEtype = MIMEtypes[0] + '/*'
204*cda5da8dSAndroid Build Coastguard Worker    if MIMEtype in caps:
205*cda5da8dSAndroid Build Coastguard Worker        entries = entries + caps[MIMEtype]
206*cda5da8dSAndroid Build Coastguard Worker    if key is not None:
207*cda5da8dSAndroid Build Coastguard Worker        entries = [e for e in entries if key in e]
208*cda5da8dSAndroid Build Coastguard Worker    entries = sorted(entries, key=lineno_sort_key)
209*cda5da8dSAndroid Build Coastguard Worker    return entries
210*cda5da8dSAndroid Build Coastguard Worker
211*cda5da8dSAndroid Build Coastguard Workerdef subst(field, MIMEtype, filename, plist=[]):
212*cda5da8dSAndroid Build Coastguard Worker    # XXX Actually, this is Unix-specific
213*cda5da8dSAndroid Build Coastguard Worker    res = ''
214*cda5da8dSAndroid Build Coastguard Worker    i, n = 0, len(field)
215*cda5da8dSAndroid Build Coastguard Worker    while i < n:
216*cda5da8dSAndroid Build Coastguard Worker        c = field[i]; i = i+1
217*cda5da8dSAndroid Build Coastguard Worker        if c != '%':
218*cda5da8dSAndroid Build Coastguard Worker            if c == '\\':
219*cda5da8dSAndroid Build Coastguard Worker                c = field[i:i+1]; i = i+1
220*cda5da8dSAndroid Build Coastguard Worker            res = res + c
221*cda5da8dSAndroid Build Coastguard Worker        else:
222*cda5da8dSAndroid Build Coastguard Worker            c = field[i]; i = i+1
223*cda5da8dSAndroid Build Coastguard Worker            if c == '%':
224*cda5da8dSAndroid Build Coastguard Worker                res = res + c
225*cda5da8dSAndroid Build Coastguard Worker            elif c == 's':
226*cda5da8dSAndroid Build Coastguard Worker                res = res + filename
227*cda5da8dSAndroid Build Coastguard Worker            elif c == 't':
228*cda5da8dSAndroid Build Coastguard Worker                if _find_unsafe(MIMEtype):
229*cda5da8dSAndroid Build Coastguard Worker                    msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
230*cda5da8dSAndroid Build Coastguard Worker                    warnings.warn(msg, UnsafeMailcapInput)
231*cda5da8dSAndroid Build Coastguard Worker                    return None
232*cda5da8dSAndroid Build Coastguard Worker                res = res + MIMEtype
233*cda5da8dSAndroid Build Coastguard Worker            elif c == '{':
234*cda5da8dSAndroid Build Coastguard Worker                start = i
235*cda5da8dSAndroid Build Coastguard Worker                while i < n and field[i] != '}':
236*cda5da8dSAndroid Build Coastguard Worker                    i = i+1
237*cda5da8dSAndroid Build Coastguard Worker                name = field[start:i]
238*cda5da8dSAndroid Build Coastguard Worker                i = i+1
239*cda5da8dSAndroid Build Coastguard Worker                param = findparam(name, plist)
240*cda5da8dSAndroid Build Coastguard Worker                if _find_unsafe(param):
241*cda5da8dSAndroid Build Coastguard Worker                    msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
242*cda5da8dSAndroid Build Coastguard Worker                    warnings.warn(msg, UnsafeMailcapInput)
243*cda5da8dSAndroid Build Coastguard Worker                    return None
244*cda5da8dSAndroid Build Coastguard Worker                res = res + param
245*cda5da8dSAndroid Build Coastguard Worker            # XXX To do:
246*cda5da8dSAndroid Build Coastguard Worker            # %n == number of parts if type is multipart/*
247*cda5da8dSAndroid Build Coastguard Worker            # %F == list of alternating type and filename for parts
248*cda5da8dSAndroid Build Coastguard Worker            else:
249*cda5da8dSAndroid Build Coastguard Worker                res = res + '%' + c
250*cda5da8dSAndroid Build Coastguard Worker    return res
251*cda5da8dSAndroid Build Coastguard Worker
252*cda5da8dSAndroid Build Coastguard Workerdef findparam(name, plist):
253*cda5da8dSAndroid Build Coastguard Worker    name = name.lower() + '='
254*cda5da8dSAndroid Build Coastguard Worker    n = len(name)
255*cda5da8dSAndroid Build Coastguard Worker    for p in plist:
256*cda5da8dSAndroid Build Coastguard Worker        if p[:n].lower() == name:
257*cda5da8dSAndroid Build Coastguard Worker            return p[n:]
258*cda5da8dSAndroid Build Coastguard Worker    return ''
259*cda5da8dSAndroid Build Coastguard Worker
260*cda5da8dSAndroid Build Coastguard Worker
261*cda5da8dSAndroid Build Coastguard Worker# Part 4: test program.
262*cda5da8dSAndroid Build Coastguard Worker
263*cda5da8dSAndroid Build Coastguard Workerdef test():
264*cda5da8dSAndroid Build Coastguard Worker    import sys
265*cda5da8dSAndroid Build Coastguard Worker    caps = getcaps()
266*cda5da8dSAndroid Build Coastguard Worker    if not sys.argv[1:]:
267*cda5da8dSAndroid Build Coastguard Worker        show(caps)
268*cda5da8dSAndroid Build Coastguard Worker        return
269*cda5da8dSAndroid Build Coastguard Worker    for i in range(1, len(sys.argv), 2):
270*cda5da8dSAndroid Build Coastguard Worker        args = sys.argv[i:i+2]
271*cda5da8dSAndroid Build Coastguard Worker        if len(args) < 2:
272*cda5da8dSAndroid Build Coastguard Worker            print("usage: mailcap [MIMEtype file] ...")
273*cda5da8dSAndroid Build Coastguard Worker            return
274*cda5da8dSAndroid Build Coastguard Worker        MIMEtype = args[0]
275*cda5da8dSAndroid Build Coastguard Worker        file = args[1]
276*cda5da8dSAndroid Build Coastguard Worker        command, e = findmatch(caps, MIMEtype, 'view', file)
277*cda5da8dSAndroid Build Coastguard Worker        if not command:
278*cda5da8dSAndroid Build Coastguard Worker            print("No viewer found for", type)
279*cda5da8dSAndroid Build Coastguard Worker        else:
280*cda5da8dSAndroid Build Coastguard Worker            print("Executing:", command)
281*cda5da8dSAndroid Build Coastguard Worker            sts = os.system(command)
282*cda5da8dSAndroid Build Coastguard Worker            sts = os.waitstatus_to_exitcode(sts)
283*cda5da8dSAndroid Build Coastguard Worker            if sts:
284*cda5da8dSAndroid Build Coastguard Worker                print("Exit status:", sts)
285*cda5da8dSAndroid Build Coastguard Worker
286*cda5da8dSAndroid Build Coastguard Workerdef show(caps):
287*cda5da8dSAndroid Build Coastguard Worker    print("Mailcap files:")
288*cda5da8dSAndroid Build Coastguard Worker    for fn in listmailcapfiles(): print("\t" + fn)
289*cda5da8dSAndroid Build Coastguard Worker    print()
290*cda5da8dSAndroid Build Coastguard Worker    if not caps: caps = getcaps()
291*cda5da8dSAndroid Build Coastguard Worker    print("Mailcap entries:")
292*cda5da8dSAndroid Build Coastguard Worker    print()
293*cda5da8dSAndroid Build Coastguard Worker    ckeys = sorted(caps)
294*cda5da8dSAndroid Build Coastguard Worker    for type in ckeys:
295*cda5da8dSAndroid Build Coastguard Worker        print(type)
296*cda5da8dSAndroid Build Coastguard Worker        entries = caps[type]
297*cda5da8dSAndroid Build Coastguard Worker        for e in entries:
298*cda5da8dSAndroid Build Coastguard Worker            keys = sorted(e)
299*cda5da8dSAndroid Build Coastguard Worker            for k in keys:
300*cda5da8dSAndroid Build Coastguard Worker                print("  %-15s" % k, e[k])
301*cda5da8dSAndroid Build Coastguard Worker            print()
302*cda5da8dSAndroid Build Coastguard Worker
303*cda5da8dSAndroid Build Coastguard Workerif __name__ == '__main__':
304*cda5da8dSAndroid Build Coastguard Worker    test()
305