Browse code

Add implementation

Robert Cranston authored on 11/03/2022 20:46:26
Showing 3 changed files

... ...
@@ -1,9 +1,155 @@
1 1
 # [`glregistry`][]
2 2
 
3
-Cache and query the [OpenGL registry][] locally.
3
+Cache and query the [OpenGL][] [registry][] locally.
4
+
5
+`glregistry` is a Python program that glues together [HTTPS][] downloading,
6
+[XPath][] querying, data accumulation/sorting, [`grep`][]ing, [color][] output,
7
+and launching external [editor][]/[pager][] programs.
8
+
9
+It tries to be a good Unix citizen by obeying relevant environment variables
10
+and being [pipe][]-friendly, outputting newline-separated entries. Where
11
+appropriate each line is in a widely supported format, usable with e.g.
12
+[Vim][]'s [QuickFix][] ([`:cexpr`][]` system('glregistry audit')` or [`vim
13
+-q`][]` <(glregistry audit)`) and [GNU Emacs][]' [Compilation Mode][] ([`M-x
14
+compile`][]` glregistry audit`).
15
+
16
+Note that only the OpenGL (not OpenGL ES) API, and only the core (not
17
+compatibility) [profile][] is considered.
4 18
 
5 19
 [`glregistry`]: https://git.rcrnstn.net/rcrnstn/glregistry
6
-[OpenGL registry]: https://www.khronos.org/registry/OpenGL/
20
+[OpenGL]: https://en.wikipedia.org/wiki/OpenGL
21
+[registry]: https://registry.khronos.org/OpenGL/
22
+[HTTPS]: https://en.wikipedia.org/wiki/HTTPS
23
+[XPath]: https://en.wikipedia.org/wiki/XPath
24
+[`grep`]: https://en.wikipedia.org/wiki/Grep
25
+[color]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
26
+[editor]: https://en.wikipedia.org/wiki/Text_editor
27
+[pager]: https://en.wikipedia.org/wiki/Terminal_pager
28
+[pipe]: https://en.wikipedia.org/wiki/Pipeline_(Unix)
29
+[Vim]: https://en.wikipedia.org/wiki/Vim_(text_editor)
30
+[QuickFix]: https://vimhelp.org/quickfix.txt.html
31
+[`:cexpr`]: https://vimhelp.org/quickfix.txt.html#%3Acexpr
32
+[`vim -q`]: https://vimhelp.org/starting.txt.html#-q
33
+[GNU Emacs]: https://en.wikipedia.org/wiki/GNU_Emacs
34
+[Compilation Mode]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Compilation-Mode.html
35
+[`M-x compile`]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Compilation.html#index-compile-2624
36
+[profile]: https://www.khronos.org/opengl/wiki/OpenGL_Context#OpenGL_3.2_and_Profiles
37
+
38
+## Usage
39
+
40
+`glregistry --help`:
41
+
42
+```
43
+glregistry 1.0
44
+
45
+Cache and query the OpenGL registry locally.
46
+
47
+Usage:
48
+  glregistry xml
49
+  glregistry xml-path
50
+  glregistry ext            <extension>
51
+  glregistry ext-path       <extension>
52
+  glregistry exts
53
+  glregistry exts-download
54
+  glregistry exts-all       <name>
55
+  glregistry type           <type>
56
+  glregistry value          <enum>
57
+  glregistry enum           <value>
58
+  glregistry supports       <name>
59
+  glregistry names          [<support>]
60
+  glregistry groups         [<enum>]
61
+  glregistry enums          [<group>]
62
+  glregistry enums-tree     [<group>]
63
+  glregistry params         [<group>]
64
+  glregistry params-tree    [<group>]
65
+  glregistry audit          [<path>]
66
+  glregistry audit-tree     [<path>]
67
+  glregistry -h|--help
68
+
69
+Commands:
70
+  xml
71
+    Download the registry XML and open it with an editor.
72
+  xml-path
73
+    Download the registry XML and print its local path.
74
+  ext <extension>
75
+    Download the <extension> spec and open it with an editor.
76
+  ext-path <extension>
77
+    Download the <extension> spec and print its local path.
78
+  exts
79
+    Print the names of all extension specs.
80
+  exts-download
81
+    Download all extension specs.
82
+  exts-all <name>
83
+    Print all downloaded extensions that mention <name>.
84
+  type <type>
85
+    Print the definition of <type>.
86
+  value <enum>
87
+    Print the value of <enum>.
88
+  enum <value>
89
+    Print the enum(s) that has the given <value>, using exact string matching.
90
+  supports <name>
91
+    Print the OpenGL version or extension required to use <name>.
92
+  names [<support>]
93
+    Print the names introduced by the OpenGL version or extension <support> if
94
+    given, or all names if omitted. The special values VERSION and EXTENSION
95
+    print the names introduced by all versions or all extensions respectively.
96
+  groups [<enum>]
97
+    Print the groups of <enum> if given, or all groups if omitted.
98
+  enums [<group>]
99
+    Print the enums in <group> if given, or all enums if omitted.
100
+  enums-tree [<group>]
101
+    Print the enums in <group> if given, or all enums if omitted, sorted on
102
+    support, in a tree.
103
+  params [<group>]
104
+    Print the parameter names of <group> if given, or all parameter names if
105
+    omitted.
106
+  params-tree [<group>]
107
+    Print the parameter names of <group> if given, or all parameter names if
108
+    omitted, sorted on count, together with the commands sorted on support, in
109
+    a tree.
110
+  audit [<path>]
111
+    Search files in <path> if given, or the current directory if omitted,
112
+    recursively for OpenGL API names and print them sorted on location,
113
+    support, and name, in a list.
114
+  audit-tree [<path>]
115
+    Search files in <path> if given, or the current directory if omitted,
116
+    recursively for OpenGL API names and print them sorted on support, name,
117
+    and location, in a tree.
118
+
119
+Environment variables:
120
+  GLREGISTRY_CACHE
121
+    The directory to cache files in. Defaults to `$XDG_CACHE_HOME/glregistry`
122
+    or, if `$XDG_CACHE_HOME` is not defined, `$HOME/.cache/glregistry`.
123
+  GLREGISTRY_EDITOR
124
+    The editor to use when opening files. Defaults to `$EDITOR` or, if
125
+    `$EDITOR` is not defined, `editor` if it exists in `$PATH`, else `vi`. The
126
+    value is interpreted by the shell.
127
+  GLREGISTRY_PAGER
128
+    The pager to use when viewing output. Defaults to `$PAGER` or, if `$PAGER`
129
+    is not defined, `pager` if it exists in `$PATH`, else `less` . The value is
130
+    interpreted by the shell. If the `$LESS` environment variable is unset, it
131
+    is set to `FR`.
132
+```
133
+
134
+## References
135
+
136
+Note that some of these might be out of date.
137
+
138
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/xml/readme.pdf>
139
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/xml/registry.rnc>
140
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/xml/reg.py>
141
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/xml/genheaders.py>
142
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/extensions/registry.py>
143
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/docs/syntaxrules.txt>
144
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/docs/promoting.html>
145
+-   <https://github.com/KhronosGroup/OpenGL-Registry/blob/main/docs/enums.html>
146
+-   <https://github.com/KhronosGroup/OpenGL-Registry/issues/481>
147
+
148
+See also:
149
+
150
+-   The community-driven [OpenGL Hardware Database][].
151
+
152
+[OpenGL Hardware Database]: https://opengl.gpuinfo.org
7 153
 
8 154
 ## Install
9 155
 
10 156
new file mode 100755
... ...
@@ -0,0 +1,498 @@
1
+#!/usr/bin/env python3
2
+
3
+
4
+## Help
5
+"""
6
+glregistry 1.0
7
+
8
+Cache and query the OpenGL registry locally.
9
+
10
+Usage:
11
+  glregistry xml
12
+  glregistry xml-path
13
+  glregistry ext            <extension>
14
+  glregistry ext-path       <extension>
15
+  glregistry exts
16
+  glregistry exts-download
17
+  glregistry exts-all       <name>
18
+  glregistry type           <type>
19
+  glregistry value          <enum>
20
+  glregistry enum           <value>
21
+  glregistry supports       <name>
22
+  glregistry names          [<support>]
23
+  glregistry groups         [<enum>]
24
+  glregistry enums          [<group>]
25
+  glregistry enums-tree     [<group>]
26
+  glregistry params         [<group>]
27
+  glregistry params-tree    [<group>]
28
+  glregistry audit          [<path>]
29
+  glregistry audit-tree     [<path>]
30
+  glregistry -h|--help
31
+
32
+Commands:
33
+  xml
34
+    Download the registry XML and open it with an editor.
35
+  xml-path
36
+    Download the registry XML and print its local path.
37
+  ext <extension>
38
+    Download the <extension> spec and open it with an editor.
39
+  ext-path <extension>
40
+    Download the <extension> spec and print its local path.
41
+  exts
42
+    Print the names of all extension specs.
43
+  exts-download
44
+    Download all extension specs.
45
+  exts-all <name>
46
+    Print all downloaded extensions that mention <name>.
47
+  type <type>
48
+    Print the definition of <type>.
49
+  value <enum>
50
+    Print the value of <enum>.
51
+  enum <value>
52
+    Print the enum(s) that has the given <value>, using exact string matching.
53
+  supports <name>
54
+    Print the OpenGL version or extension required to use <name>.
55
+  names [<support>]
56
+    Print the names introduced by the OpenGL version or extension <support> if
57
+    given, or all names if omitted. The special values VERSION and EXTENSION
58
+    print the names introduced by all versions or all extensions respectively.
59
+  groups [<enum>]
60
+    Print the groups of <enum> if given, or all groups if omitted.
61
+  enums [<group>]
62
+    Print the enums in <group> if given, or all enums if omitted.
63
+  enums-tree [<group>]
64
+    Print the enums in <group> if given, or all enums if omitted, sorted on
65
+    support, in a tree.
66
+  params [<group>]
67
+    Print the parameter names of <group> if given, or all parameter names if
68
+    omitted.
69
+  params-tree [<group>]
70
+    Print the parameter names of <group> if given, or all parameter names if
71
+    omitted, sorted on count, together with the commands sorted on support, in
72
+    a tree.
73
+  audit [<path>]
74
+    Search files in <path> if given, or the current directory if omitted,
75
+    recursively for OpenGL API names and print them sorted on location,
76
+    support, and name, in a list.
77
+  audit-tree [<path>]
78
+    Search files in <path> if given, or the current directory if omitted,
79
+    recursively for OpenGL API names and print them sorted on support, name,
80
+    and location, in a tree.
81
+
82
+Environment variables:
83
+  GLREGISTRY_CACHE
84
+    The directory to cache files in. Defaults to `$XDG_CACHE_HOME/glregistry`
85
+    or, if `$XDG_CACHE_HOME` is not defined, `$HOME/.cache/glregistry`.
86
+  GLREGISTRY_EDITOR
87
+    The editor to use when opening files. Defaults to `$EDITOR` or, if
88
+    `$EDITOR` is not defined, `editor` if it exists in `$PATH`, else `vi`. The
89
+    value is interpreted by the shell.
90
+  GLREGISTRY_PAGER
91
+    The pager to use when viewing output. Defaults to `$PAGER` or, if `$PAGER`
92
+    is not defined, `pager` if it exists in `$PATH`, else `less` . The value is
93
+    interpreted by the shell. If the `$LESS` environment variable is unset, it
94
+    is set to `FR`.
95
+"""
96
+
97
+
98
+## Imports
99
+import os
100
+import sys
101
+import collections
102
+import re
103
+import subprocess
104
+import shutil
105
+import shlex
106
+import urllib.request
107
+import docopt
108
+import lxml.etree
109
+
110
+
111
+## Constants
112
+REGISTRY_URL  = 'https://registry.khronos.org/OpenGL/'
113
+XML_PATH      = 'xml/gl.xml'
114
+REGEX         = r'\b(gl|GL_)[0-9A-Z][0-9A-Za-z_]+\b'
115
+EXCLUDE_DIRS  = ['.?*', '_*']
116
+EXCLUDE_FILES = ['README*', 'TODO*']
117
+INDENT        = 2
118
+ENV_XDG = lambda var, default: os.environ.get(
119
+    f'GLREGISTRY_{var}',
120
+    os.path.join(
121
+        os.environ.get(
122
+            f'XDG_{var}_HOME',
123
+            os.path.expanduser(default),
124
+        ),
125
+        'glregistry',
126
+    ),
127
+)
128
+ENV_PRG = lambda var, default: os.environ.get(
129
+    f'GLREGISTRY_{var}',
130
+    os.environ.get(
131
+        var,
132
+        shutil.which(var.lower()) or default,
133
+    ),
134
+)
135
+CACHE  = ENV_XDG('CACHE',  os.path.join('~', '.cache'))
136
+EDITOR = ENV_PRG('EDITOR', 'vi')
137
+PAGER  = ENV_PRG('PAGER',  'less')
138
+LESS   = os.environ.get('LESS', 'FR')
139
+IN    = lambda a, v, s: f"contains(concat('{s}',@{a},'{s}'),'{s}{v}{s}')"
140
+MAYBE = lambda a, v:    f"(@{a}='{v}' or not(@{a}))"
141
+TYPES    = "/registry/types"
142
+ENUMS    = "/registry/enums"
143
+COMMANDS = "/registry/commands"
144
+CATEGORY_ATTRIBS = {
145
+    'VERSION': [
146
+        "/registry/feature[@api='gl']",
147
+        'number',
148
+    ],
149
+    'EXTENSION': [
150
+        f"/registry/extensions/extension[{IN('supported','gl','|')}]",
151
+        'name',
152
+    ],
153
+}
154
+REQUIRE = f"require[{MAYBE('api','gl')} and {MAYBE('profile','core')}]"
155
+
156
+
157
+## Helpers
158
+
159
+
160
+### `log`
161
+def log(*args, **kwargs):
162
+    print(*args, file=sys.stderr, flush=True, **kwargs)
163
+
164
+
165
+### `edit`
166
+def edit(paths):
167
+    if EDITOR and sys.stdout.isatty():
168
+        args = ' '.join([EDITOR, *map(shlex.quote, paths)])
169
+        subprocess.run(args, shell=True)
170
+    else:
171
+        for path in paths:
172
+            with open(path) as f:
173
+                shutil.copyfileobj(f, sys.stdout)
174
+
175
+
176
+### `page`
177
+def page(lines):
178
+    lines = ''.join(f'{line}\n' for line in lines)
179
+    if lines and PAGER and sys.stdout.isatty():
180
+        args = f'LESS={shlex.quote(LESS)} {PAGER}'
181
+        subprocess.run(args, shell=True, text=True, input=lines)
182
+    else:
183
+        sys.stdout.write(lines)
184
+
185
+
186
+### `indentjoin`
187
+def indentjoin(indent, sep, parts):
188
+    return ' ' * INDENT * indent + sep.join(map(str, parts))
189
+
190
+
191
+### `removeprefix`
192
+def removeprefix(prefix, string):
193
+    if string.startswith(prefix):
194
+        return string[len(prefix):]
195
+    return string
196
+
197
+
198
+### `download`
199
+def download(path, exit_on_failure=True):
200
+    remote = urllib.parse.urljoin(REGISTRY_URL, path)
201
+    local  = os.path.join        (CACHE,        path)
202
+    if not os.path.exists(local):
203
+        try:
204
+            log(f"Downloading '{path}' ... ", end='')
205
+            with urllib.request.urlopen(remote) as response:
206
+                os.makedirs(os.path.dirname(local), exist_ok=True)
207
+                with open(local, 'wb') as f:
208
+                    shutil.copyfileobj(response, f)
209
+        except urllib.error.URLError as error:
210
+            log(error.reason)
211
+            if exit_on_failure:
212
+                exit(1)
213
+        else:
214
+            log(response.reason)
215
+    return local
216
+
217
+
218
+### `grep`
219
+def grep(
220
+    path=None,
221
+    regex=REGEX,
222
+    exclude_dirs=EXCLUDE_DIRS,
223
+    exclude_files=EXCLUDE_FILES,
224
+    silent=False,
225
+):
226
+    path          = path if path else '.'
227
+    cmd           = ['grep', '-EIrno']
228
+    exclude_dirs  = [f'--exclude-dir={exclude}' for exclude in exclude_dirs]
229
+    exclude_files = [f'--exclude={exclude}'     for exclude in exclude_files]
230
+    process = subprocess.run(
231
+        [*cmd, *exclude_dirs, *exclude_files, regex, path],
232
+        stdout=subprocess.PIPE,
233
+        stderr=subprocess.DEVNULL if silent else None,
234
+        text=True,
235
+    )
236
+    for string in process.stdout.splitlines():
237
+        string           = removeprefix(f'.{os.path.sep}', string)
238
+        file, line, name = string.split(':', 2)
239
+        line             = int(line)
240
+        yield file, line, name
241
+
242
+
243
+## Commands
244
+
245
+
246
+### `xml_`
247
+def xml_():
248
+    return lxml.etree.parse(download(XML_PATH))
249
+
250
+
251
+### `xml`
252
+def xml():
253
+    return [download(XML_PATH)]
254
+
255
+
256
+### `ext_`
257
+def ext_(extension):
258
+    prefix, vendor, name = extension.split('_', 2)
259
+    if prefix != 'GL':
260
+        log("Extension names must start with 'GL_'.")
261
+        exit(1)
262
+    return f'extensions/{vendor}/{vendor}_{name}.txt'
263
+
264
+
265
+### `ext`
266
+def ext(extension):
267
+    return [download(ext_(extension))]
268
+
269
+
270
+### `exts`
271
+def exts(xml):
272
+    category, attrib = CATEGORY_ATTRIBS['EXTENSION']
273
+    exts = xml.xpath(f"{category}/@{attrib}")
274
+    return sorted(exts)
275
+
276
+
277
+### `exts_download`
278
+def exts_download(xml):
279
+    for ext in exts(xml):
280
+        download(ext_(ext), exit_on_failure=False)
281
+    return []
282
+
283
+
284
+### `exts_all`
285
+def exts_all(xml, name):
286
+    # exts_download(xml)
287
+    exts_all = set(
288
+        'GL_' + os.path.splitext(os.path.basename(file))[0]
289
+        for name in set([
290
+            name,
291
+            removeprefix('gl',  name),
292
+            removeprefix('GL_', name),
293
+        ])
294
+        for file, *_ in
295
+        grep(os.path.join(CACHE, 'extensions'), rf'\b{name}\b', [], [])
296
+    )
297
+    return sorted(exts_all)
298
+
299
+
300
+### `type`
301
+def type(xml, type):
302
+    return [xml.xpath(f"string({TYPES}/type/name[text()='{type}']/..)")]
303
+
304
+
305
+### `value`
306
+def value(xml, enum):
307
+    return xml.xpath(f"{ENUMS}/enum[@name='{enum}']/@value")
308
+
309
+
310
+### `enum`
311
+def enum(xml, value):
312
+    enum = xml.xpath(f"{ENUMS}/enum[@value='{value}']/@name")
313
+    return sorted(enum)
314
+
315
+
316
+### `supports`
317
+def supports(xml, name):
318
+    category, attrib = CATEGORY_ATTRIBS['EXTENSION']
319
+    if xml.xpath(f"{category}[@{attrib}='{name}']"):
320
+        return ['EXTENSION']
321
+    supports_ = [
322
+        support
323
+        for category, attrib in CATEGORY_ATTRIBS.values()
324
+        for support in
325
+        xml.xpath(f"{category}/{REQUIRE}/*[@name='{name}']/../../@{attrib}")
326
+    ]
327
+    return sorted(supports_)
328
+
329
+
330
+### `names`
331
+def names(xml, support=None):
332
+    if support in CATEGORY_ATTRIBS.keys():
333
+        category_attribs = [
334
+            [category, ""]
335
+            for category, _ in [CATEGORY_ATTRIBS[support]]
336
+        ]
337
+    elif support:
338
+        category_attribs = [
339
+            [category, f"[@{attrib}='{support}']"]
340
+            for category, attrib in CATEGORY_ATTRIBS.values()
341
+        ]
342
+    else:
343
+        category_attribs = [
344
+            [category, ""]
345
+            for category, _ in CATEGORY_ATTRIBS.values()
346
+        ]
347
+    names = set(
348
+        name
349
+        for category, attrib in category_attribs
350
+        for name in
351
+        xml.xpath(f"{category}{attrib}/{REQUIRE}/*/@name")
352
+    )
353
+    return sorted(names)
354
+
355
+
356
+### `groups`
357
+def groups(xml, enum=None):
358
+    name = f"[@name='{enum}']" if enum else ""
359
+    return sorted(set(
360
+        group
361
+        for groups in xml.xpath(f"{ENUMS}/enum{name}/@group")
362
+        for group  in groups.split(',')
363
+    ))
364
+
365
+
366
+### `enums_`
367
+def enums_(xml, group=None):
368
+    group = f"[{IN('group',group,',')}]" if group else ""
369
+    enums_ = collections.defaultdict(list)
370
+    for enum in xml.xpath(f"{ENUMS}/enum{group}/@name"):
371
+        supports_ = supports(xml, enum)
372
+        if supports_:
373
+            enums_[tuple(supports_)].append(enum)
374
+    return enums_
375
+
376
+
377
+### `enums`
378
+def enums(xml, group=None):
379
+    enums = [
380
+        enum
381
+        for _, enums in enums_(xml, group).items()
382
+        for enum     in enums
383
+    ]
384
+    return sorted(enums)
385
+
386
+
387
+### `enums_tree`
388
+def enums_tree(xml, group=None):
389
+    for supports, enums in sorted(enums_(xml, group).items()):
390
+        yield indentjoin(0, ',', supports)
391
+        for enum in sorted(enums):
392
+            yield indentjoin(1, '', [enum])
393
+
394
+
395
+### `params_`
396
+def params_(xml, group=None):
397
+    group   = f"[@group='{group}']" if group else ""
398
+    counts  = collections.defaultdict(int)
399
+    params_ = collections.defaultdict(lambda: collections.defaultdict(list))
400
+    for xmlcommand in xml.xpath(f"{COMMANDS}/command/param{group}/.."):
401
+        command   = xmlcommand.xpath(f"string(proto/name)")
402
+        supports_ = supports(xml, command)
403
+        if supports_:
404
+            for xmlparam in xmlcommand.xpath(f"param{group}"):
405
+                param = xmlparam.xpath(f"string(name)")
406
+                params_[param][tuple(supports_)].append(command)
407
+                counts[param] -= 1
408
+    return {
409
+        (count, param): params_[param]
410
+        for param, count in counts.items()
411
+    }
412
+
413
+
414
+### `params`
415
+def params(xml, group=None):
416
+    params = [param for (_, param), _ in params_(xml, group).items()]
417
+    return sorted(params)
418
+
419
+
420
+### `params_tree`
421
+def params_tree(xml, group=None):
422
+    for (count, param), occurences in sorted(params_(xml, group).items()):
423
+        yield indentjoin(0, ':', [
424
+            param,
425
+            -count,
426
+        ])
427
+        for supports_, commands in sorted(occurences.items()):
428
+            yield indentjoin(1, ',', supports_)
429
+            for command in sorted(commands):
430
+                yield indentjoin(2, '', [command])
431
+
432
+
433
+### `audit_`
434
+def audit_(xml, path=None):
435
+    audit_ = collections.defaultdict(lambda: collections.defaultdict(list))
436
+    for file, line, name in grep(path):
437
+        supports_ = supports(xml, name)
438
+        if not supports_:
439
+            supports_ = ['UNSUPPORTED']
440
+        audit_[tuple(supports_)][name].append([file, line])
441
+    return audit_
442
+
443
+
444
+### `audit`
445
+def audit(xml, path=None):
446
+    for file, line, supports, name in sorted(
447
+        [file, line, supports, name]
448
+        for supports, names  in audit_(xml, path).items()
449
+        for name, locations  in names.items()
450
+        for file, line       in locations
451
+    ):
452
+        yield indentjoin(0, ':', [
453
+            file,
454
+            line,
455
+            indentjoin(0, ',', supports),
456
+            name
457
+        ])
458
+
459
+
460
+### `audit_tree`
461
+def audit_tree(xml, path=None):
462
+    for supports, names in sorted(audit_(xml, path).items()):
463
+        yield indentjoin(0, ',', supports)
464
+        for name, locations in sorted(names.items()):
465
+            yield indentjoin(1, '', [name])
466
+            for file, line in sorted(locations):
467
+                yield indentjoin(2, ':', [
468
+                    file,
469
+                    line,
470
+                ])
471
+
472
+
473
+## Main
474
+def main():
475
+    args = docopt.docopt(__doc__)
476
+    if args['xml']:           edit(xml          ())
477
+    if args['xml-path']:      page(xml          ())
478
+    if args['ext']:           edit(ext          (args['<extension>']))
479
+    if args['ext-path']:      page(ext          (args['<extension>']))
480
+    if args['exts']:          page(exts         (xml_()))
481
+    if args['exts-download']: page(exts_download(xml_()))
482
+    if args['exts-all']:      page(exts_all     (xml_(), args['<name>']))
483
+    if args['type']:          page(type         (xml_(), args['<type>']))
484
+    if args['value']:         page(value        (xml_(), args['<enum>']))
485
+    if args['enum']:          page(enum         (xml_(), args['<value>']))
486
+    if args['supports']:      page(supports     (xml_(), args['<name>']))
487
+    if args['names']:         page(names        (xml_(), args['<support>']))
488
+    if args['groups']:        page(groups       (xml_(), args['<enum>']))
489
+    if args['enums']:         page(enums        (xml_(), args['<group>']))
490
+    if args['enums-tree']:    page(enums_tree   (xml_(), args['<group>']))
491
+    if args['params']:        page(params       (xml_(), args['<group>']))
492
+    if args['params-tree']:   page(params_tree  (xml_(), args['<group>']))
493
+    if args['audit']:         page(audit        (xml_(), args['<path>']))
494
+    if args['audit-tree']:    page(audit_tree   (xml_(), args['<path>']))
495
+
496
+
497
+if __name__ == '__main__':
498
+    main()
... ...
@@ -19,6 +19,8 @@ setup(
19 19
     ],
20 20
     python_requires='>=3, <4',
21 21
     install_requires=[
22
+        'docopt',
23
+        'lxml',
22 24
     ],
23 25
     py_modules=['glregistry'],
24 26
     entry_points={