Browse code

Add implementation

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

... ...
@@ -2,9 +2,154 @@
2 2
 
3 3
 Cache and query the [OpenGL][] [registry][] locally.
4 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.
18
+
5 19
 [`glregistry`]: https://git.rcrnstn.net/rcrnstn/glregistry
6 20
 [OpenGL]: https://en.wikipedia.org/wiki/OpenGL
7 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
8 153
 
9 154
 ## Install
10 155
 
11 156
new file mode 100755
... ...
@@ -0,0 +1,497 @@
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
+from lxml import 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: (
119
+    os.environ.get(f'GLREGISTRY_{var}') or
120
+    os.path.join(
121
+        (
122
+            os.environ.get(f'XDG_{var}_HOME') or
123
+            os.path.expanduser(default)
124
+        ),
125
+        'glregistry',
126
+    )
127
+)
128
+ENV_PRG = lambda var, default: (
129
+    os.environ.get(f'GLREGISTRY_{var}') or
130
+    os.environ.get(var) or
131
+    shutil.which(var.lower()) or
132
+    default
133
+)
134
+CACHE  = ENV_XDG('CACHE',  os.path.join('~', '.cache'))
135
+EDITOR = ENV_PRG('EDITOR', 'vi')
136
+PAGER  = ENV_PRG('PAGER',  'less')
137
+LESS   = os.environ.get('LESS') or 'FR'
138
+IN    = lambda a, v, s: f"contains(concat('{s}',@{a},'{s}'),'{s}{v}{s}')"
139
+MAYBE = lambda a, v:    f"(@{a}='{v}' or not(@{a}))"
140
+TYPES    = "/registry/types"
141
+ENUMS    = "/registry/enums"
142
+COMMANDS = "/registry/commands"
143
+CATEGORY_ATTRIBS = {
144
+    'VERSION': [
145
+        "/registry/feature[@api='gl']",
146
+        'number',
147
+    ],
148
+    'EXTENSION': [
149
+        f"/registry/extensions/extension[{IN('supported','gl','|')}]",
150
+        'name',
151
+    ],
152
+}
153
+REQUIRE = f"require[{MAYBE('api','gl')} and {MAYBE('profile','core')}]"
154
+
155
+
156
+## Helpers
157
+
158
+
159
+### `log`
160
+def log(*args, **kwargs):
161
+    print(*args, file=sys.stderr, flush=True, **kwargs)
162
+
163
+
164
+### `edit`
165
+def edit(paths):
166
+    if EDITOR and sys.stdout.isatty():
167
+        args = ' '.join([EDITOR, *map(shlex.quote, paths)])
168
+        subprocess.run(args, shell=True)
169
+    else:
170
+        for path in paths:
171
+            with open(path) as f:
172
+                shutil.copyfileobj(f, sys.stdout)
173
+
174
+
175
+### `page`
176
+def page(lines):
177
+    lines = ''.join(f'{line}\n' for line in lines)
178
+    if lines and PAGER and sys.stdout.isatty():
179
+        args = f'LESS={shlex.quote(LESS)} {PAGER}'
180
+        subprocess.run(args, shell=True, text=True, input=lines)
181
+    else:
182
+        sys.stdout.write(lines)
183
+
184
+
185
+### `indentjoin`
186
+def indentjoin(indent, sep, parts):
187
+    return ' ' * INDENT * indent + sep.join(map(str, parts))
188
+
189
+
190
+### `removeprefix`
191
+def removeprefix(prefix, string):
192
+    if string.startswith(prefix):
193
+        return string[len(prefix):]
194
+    return string
195
+
196
+
197
+### `download`
198
+def download(path, exit_on_failure=True):
199
+    remote = urllib.parse.urljoin(REGISTRY_URL, path)
200
+    local  = os.path.join        (CACHE,        path)
201
+    if not os.path.exists(local):
202
+        try:
203
+            log(f"Downloading '{path}' ... ", end='')
204
+            with urllib.request.urlopen(remote) as response:
205
+                os.makedirs(os.path.dirname(local), exist_ok=True)
206
+                with open(local, 'wb') as f:
207
+                    shutil.copyfileobj(response, f)
208
+        except urllib.error.URLError as error:
209
+            log(error.reason)
210
+            if exit_on_failure:
211
+                exit(1)
212
+        else:
213
+            log(response.reason)
214
+    return local
215
+
216
+
217
+### `grep`
218
+def grep(
219
+    path=None,
220
+    regex=REGEX,
221
+    exclude_dirs=EXCLUDE_DIRS,
222
+    exclude_files=EXCLUDE_FILES,
223
+    silent=False,
224
+):
225
+    path          = path if path else '.'
226
+    cmd           = ['grep', '-EIrno']
227
+    exclude_dirs  = [f'--exclude-dir={exclude}' for exclude in exclude_dirs]
228
+    exclude_files = [f'--exclude={exclude}'     for exclude in exclude_files]
229
+    process = subprocess.run(
230
+        [*cmd, *exclude_dirs, *exclude_files, regex, path],
231
+        stdout=subprocess.PIPE,
232
+        stderr=subprocess.DEVNULL if silent else None,
233
+        text=True,
234
+    )
235
+    for string in process.stdout.splitlines():
236
+        string           = removeprefix(f'.{os.path.sep}', string)
237
+        file, line, name = string.split(':', 2)
238
+        line             = int(line)
239
+        yield file, line, name
240
+
241
+
242
+## Commands
243
+
244
+
245
+### `xml_`
246
+def xml_():
247
+    return etree.parse(download(XML_PATH))
248
+
249
+
250
+### `xml`
251
+def xml():
252
+    return [download(XML_PATH)]
253
+
254
+
255
+### `ext_`
256
+def ext_(extension):
257
+    prefix, vendor, name = extension.split('_', 2)
258
+    if prefix != 'GL':
259
+        log("Extension names must start with 'GL_'.")
260
+        exit(1)
261
+    return f'extensions/{vendor}/{vendor}_{name}.txt'
262
+
263
+
264
+### `ext`
265
+def ext(extension):
266
+    return [download(ext_(extension))]
267
+
268
+
269
+### `exts`
270
+def exts(xml):
271
+    category, attrib = CATEGORY_ATTRIBS['EXTENSION']
272
+    exts = xml.xpath(f"{category}/@{attrib}")
273
+    return sorted(exts)
274
+
275
+
276
+### `exts_download`
277
+def exts_download(xml):
278
+    for ext in exts(xml):
279
+        download(ext_(ext), exit_on_failure=False)
280
+    return []
281
+
282
+
283
+### `exts_all`
284
+def exts_all(xml, name):
285
+    # exts_download(xml)
286
+    exts_all = set(
287
+        'GL_' + os.path.splitext(os.path.basename(file))[0]
288
+        for name in set([
289
+            name,
290
+            removeprefix('gl',  name),
291
+            removeprefix('GL_', name),
292
+        ])
293
+        for file, *_ in
294
+        grep(os.path.join(CACHE, 'extensions'), rf'\b{name}\b', [], [])
295
+    )
296
+    return sorted(exts_all)
297
+
298
+
299
+### `type`
300
+def type(xml, type):
301
+    return [xml.xpath(f"string({TYPES}/type/name[text()='{type}']/..)")]
302
+
303
+
304
+### `value`
305
+def value(xml, enum):
306
+    return xml.xpath(f"{ENUMS}/enum[@name='{enum}']/@value")
307
+
308
+
309
+### `enum`
310
+def enum(xml, value):
311
+    enum = xml.xpath(f"{ENUMS}/enum[@value='{value}']/@name")
312
+    return sorted(enum)
313
+
314
+
315
+### `supports`
316
+def supports(xml, name):
317
+    category, attrib = CATEGORY_ATTRIBS['EXTENSION']
318
+    if xml.xpath(f"{category}[@{attrib}='{name}']"):
319
+        return ['EXTENSION']
320
+    supports_ = [
321
+        support
322
+        for category, attrib in CATEGORY_ATTRIBS.values()
323
+        for support in
324
+        xml.xpath(f"{category}/{REQUIRE}/*[@name='{name}']/../../@{attrib}")
325
+    ]
326
+    return sorted(supports_)
327
+
328
+
329
+### `names`
330
+def names(xml, support=None):
331
+    if support in CATEGORY_ATTRIBS.keys():
332
+        category_attribs = [
333
+            [category, ""]
334
+            for category, _ in [CATEGORY_ATTRIBS[support]]
335
+        ]
336
+    elif support:
337
+        category_attribs = [
338
+            [category, f"[@{attrib}='{support}']"]
339
+            for category, attrib in CATEGORY_ATTRIBS.values()
340
+        ]
341
+    else:
342
+        category_attribs = [
343
+            [category, ""]
344
+            for category, _ in CATEGORY_ATTRIBS.values()
345
+        ]
346
+    names = set(
347
+        name
348
+        for category, attrib in category_attribs
349
+        for name in
350
+        xml.xpath(f"{category}{attrib}/{REQUIRE}/*/@name")
351
+    )
352
+    return sorted(names)
353
+
354
+
355
+### `groups`
356
+def groups(xml, enum=None):
357
+    name = f"[@name='{enum}']" if enum else ""
358
+    return sorted(set(
359
+        group
360
+        for groups in xml.xpath(f"{ENUMS}/enum{name}/@group")
361
+        for group  in groups.split(',')
362
+    ))
363
+
364
+
365
+### `enums_`
366
+def enums_(xml, group=None):
367
+    group = f"[{IN('group',group,',')}]" if group else ""
368
+    enums_ = collections.defaultdict(list)
369
+    for enum in xml.xpath(f"{ENUMS}/enum{group}/@name"):
370
+        supports_ = supports(xml, enum)
371
+        if supports_:
372
+            enums_[tuple(supports_)].append(enum)
373
+    return enums_
374
+
375
+
376
+### `enums`
377
+def enums(xml, group=None):
378
+    enums = [
379
+        enum
380
+        for _, enums in enums_(xml, group).items()
381
+        for enum     in enums
382
+    ]
383
+    return sorted(enums)
384
+
385
+
386
+### `enums_tree`
387
+def enums_tree(xml, group=None):
388
+    for supports, enums in sorted(enums_(xml, group).items()):
389
+        yield indentjoin(0, ',', supports)
390
+        for enum in sorted(enums):
391
+            yield indentjoin(1, '', [enum])
392
+
393
+
394
+### `params_`
395
+def params_(xml, group=None):
396
+    group   = f"[@group='{group}']" if group else ""
397
+    counts  = collections.defaultdict(int)
398
+    params_ = collections.defaultdict(lambda: collections.defaultdict(list))
399
+    for xmlcommand in xml.xpath(f"{COMMANDS}/command/param{group}/.."):
400
+        command   = xmlcommand.xpath(f"string(proto/name)")
401
+        supports_ = supports(xml, command)
402
+        if supports_:
403
+            for xmlparam in xmlcommand.xpath(f"param{group}"):
404
+                param = xmlparam.xpath(f"string(name)")
405
+                params_[param][tuple(supports_)].append(command)
406
+                counts[param] -= 1
407
+    return {
408
+        (count, param): params_[param]
409
+        for param, count in counts.items()
410
+    }
411
+
412
+
413
+### `params`
414
+def params(xml, group=None):
415
+    params = [param for (_, param), _ in params_(xml, group).items()]
416
+    return sorted(params)
417
+
418
+
419
+### `params_tree`
420
+def params_tree(xml, group=None):
421
+    for (count, param), occurences in sorted(params_(xml, group).items()):
422
+        yield indentjoin(0, ':', [
423
+            param,
424
+            -count,
425
+        ])
426
+        for supports_, commands in sorted(occurences.items()):
427
+            yield indentjoin(1, ',', supports_)
428
+            for command in sorted(commands):
429
+                yield indentjoin(2, '', [command])
430
+
431
+
432
+### `audit_`
433
+def audit_(xml, path=None):
434
+    audit_ = collections.defaultdict(lambda: collections.defaultdict(list))
435
+    for file, line, name in grep(path):
436
+        supports_ = supports(xml, name)
437
+        if not supports_:
438
+            supports_ = ['UNSUPPORTED']
439
+        audit_[tuple(supports_)][name].append([file, line])
440
+    return audit_
441
+
442
+
443
+### `audit`
444
+def audit(xml, path=None):
445
+    for file, line, supports, name in sorted(
446
+        [file, line, supports, name]
447
+        for supports, names  in audit_(xml, path).items()
448
+        for name, locations  in names.items()
449
+        for file, line       in locations
450
+    ):
451
+        yield indentjoin(0, ':', [
452
+            file,
453
+            line,
454
+            indentjoin(0, ',', supports),
455
+            name
456
+        ])
457
+
458
+
459
+### `audit_tree`
460
+def audit_tree(xml, path=None):
461
+    for supports, names in sorted(audit_(xml, path).items()):
462
+        yield indentjoin(0, ',', supports)
463
+        for name, locations in sorted(names.items()):
464
+            yield indentjoin(1, '', [name])
465
+            for file, line in sorted(locations):
466
+                yield indentjoin(2, ':', [
467
+                    file,
468
+                    line,
469
+                ])
470
+
471
+
472
+## Main
473
+def main():
474
+    args = docopt.docopt(__doc__)
475
+    if args['xml']:           edit(xml          ())
476
+    if args['xml-path']:      page(xml          ())
477
+    if args['ext']:           edit(ext          (args['<extension>']))
478
+    if args['ext-path']:      page(ext          (args['<extension>']))
479
+    if args['exts']:          page(exts         (xml_()))
480
+    if args['exts-download']: page(exts_download(xml_()))
481
+    if args['exts-all']:      page(exts_all     (xml_(), args['<name>']))
482
+    if args['type']:          page(type         (xml_(), args['<type>']))
483
+    if args['value']:         page(value        (xml_(), args['<enum>']))
484
+    if args['enum']:          page(enum         (xml_(), args['<value>']))
485
+    if args['supports']:      page(supports     (xml_(), args['<name>']))
486
+    if args['names']:         page(names        (xml_(), args['<support>']))
487
+    if args['groups']:        page(groups       (xml_(), args['<enum>']))
488
+    if args['enums']:         page(enums        (xml_(), args['<group>']))
489
+    if args['enums-tree']:    page(enums_tree   (xml_(), args['<group>']))
490
+    if args['params']:        page(params       (xml_(), args['<group>']))
491
+    if args['params-tree']:   page(params_tree  (xml_(), args['<group>']))
492
+    if args['audit']:         page(audit        (xml_(), args['<path>']))
493
+    if args['audit-tree']:    page(audit_tree   (xml_(), args['<path>']))
494
+
495
+
496
+if __name__ == '__main__':
497
+    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={