| ... | ... | 
                    @@ -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()  |