style: Silenced Cppcheck warnings
[para.git] / scripts / mkvars
blob16d6c591f0740ed3bc653cc609932aeb15dc43d5
1 #!/usr/bin/env python3
2 """Generate variable test cases"""
5 # Copyright 2024 Odin Kroeger.
7 # This file is part of Para.
9 # Para is free softwareyou can redistribute it and/or modify it under
10 # the terms of the GNU General Public License as published by the Free
11 # Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # Para is distributed in the hope that it will be useful, but WITHOUT
15 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17 # License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with Para. If not, see <https://www.gnu.org/licenses/>.
23 from collections.abc import Generator
24 from ctypes import (RTLD_LOCAL, CDLL, c_char_p, c_int)
25 from ctypes.util import find_library
26 from filecmp import cmp
27 from getopt import GetoptError, getopt
28 from itertools import chain, combinations, filterfalse, permutations
29 from locale import getpreferredencoding
30 from os import remove, rename, strerror
31 from os.path import basename, commonprefix, dirname, exists, isfile
32 from sys import argv, exit, stdout, version_info
33 from tempfile import NamedTemporaryFile
34 from typing import Callable, Iterable, Final, NoReturn, TypeVar
36 import logging
37 import re
39 if version_info < (3, 9):
40     exit('mkvars requires Python 3.9 or later')
44 # Metadata
47 __author__ = 'Odin Kroeger'
48 __copyright__ = '2024 Odin Kroeger'
49 __license__ = 'GPLv3+'
53 # Types
56 T = TypeVar("T")
60 # Globals
63 CQUOTEMAP: Final[dict[str, str]] = {
64     '\\': '\\\\',
65     '"': r'\"',
66     '\t': r'\t',
67     '\n': r'\n',
68     '\0': r'\0'
70 """Mapping of characters that need quoting to escape sequences"""
72 # Para does not handle newlines like Bourne shells do.
73 # Characters repeat so that they can occur more than once.
74 METACHARS: Final[tuple[str, ...]] = ('=', '=')
75 """Word-splitting meta characters"""
77 VARRE: Final[re.Pattern] = re.compile('^[a-z_][0-9a-z_]*=', flags=re.I)
78 """Expression that matches valid name=value pairs"""
82 # Classes
85 class LibC(CDLL):
86     """LibC interface."""
88     def __init__(self):
89         fname = find_library('c')
90         if not fname:
91             raise Error('Could not find LibC')
93         super().__init__(fname, mode=RTLD_LOCAL)
94         self.putenv.argtypes = (c_char_p,)
95         self.putenv.restype = c_int
99 # Errors
102 class Error(Exception):
103     """Class for errors."""
107 # Functions
110 def cquote(string: str) -> str:
111     """Quotes a string so it can be used in a C source file"""
112     return '"' + ''.join(CQUOTEMAP.get(c, c) for c in string) + '"'
115 def error(*args, status=1, **kwargs) -> NoReturn:
116     """Log an err and :func:`exit <sys.exit>` with `status`.
118     Arguments:
119     argsPositional arguments for :func:`logging.error`.
120     statusExit status.
121     kwargsKeyword arguments for :func:`logging.error`.
122     """
123     logging.error(*args, **kwargs)
124     exit(status)
127 def unique(iterable: Iterable[T]) -> Generator[T]:
128     """Return every element from `iterable`, but only once."""
129     seen: set[T] = set()
130     for elem in filterfalse(seen.__contains__, iterable):
131         seen.add(elem)
132         yield elem
135 def powerset(iterable: Iterable[T]) -> Generator[tuple[T, ...]]:
136     """Computes the powerset of `iterable`"""
137     items = tuple(iterable)
138     combs = (combinations(items, r) for r in range(len(items) + 1))
139     yield from chain.from_iterable(combs)
142 def showhelp(func: Callable):
143     """Print the docstring of `func` and :func:`exit <sys.exit>`."""
144     assert func.__doc__
145     lines = func.__doc__.splitlines()
146     prefix = commonprefix([line for line in lines[1:-1] if line])
147     nlines = len(lines)
148     for i, line in enumerate(lines):
149         line = line.removeprefix(prefix)
150         if not (i == nlines - 1 and not line):
151             print(line)
152     exit()
156 # Main
159 def main():
160     """mkvars - Create X macros for testing putvar
162     Usage: mkvars [word ...]
164     Options:
165         -o file  Save output to file.
166     """
167     progname = basename(argv[0])
168     logging.basicConfig(format=f'{progname}: %(message)s')
170     output = ''
171     try:
172         opts, args = getopt(argv[1:], 'o:h')
173     except GetoptError as err:
174         error(err, status=2)
176     for opt, optarg in opts:
177         if opt == '-h':
178             showhelp(main)
179         elif opt == '-o':
180             output = optarg
182     encoding = getpreferredencoding()
183     libc = LibC()
185     tokens = ((tuple(args) if args else
186                ('foo', 'foo', '_', '.', '/', '0')) + METACHARS)
188     tmp = None
189     if output:
190         if exists(output) and not isfile(output):
191             file = open(output, 'w', encoding=encoding)
192         else:
193             tmp = NamedTemporaryFile('w+', delete=False, encoding=encoding,
194                                      dir=dirname(output),
195                                      prefix='.', suffix='.tmp')
196             file = tmp  # type: ignore
197     else:
198         file = stdout   # type: ignore
200     try:
201         print('/* Computed putvar test cases */', file=file)
202         for sub in unique(powerset(tokens)):
203             for perm in unique(permutations(sub)):
204                 var = ''.join(perm)
205                 cvar = cquote(var)
206                 if VARRE.match(var) and libc.putenv(var.encode(encoding)) == 0:
207                     print(f'X({cvar})', file=file)
208                 else:
209                     print(f'Y({cvar})', file=file)
210     except:  # noqa
211         if tmp:
212             remove(tmp.name)
213         raise
214     try:
215         if tmp and not (exists(output) and cmp(file.name, output)):
216             rename(tmp.name, output)
217     except FileNotFoundError as err:
218         error(f'{err.filename}: {strerror(err.errno)}')
219     except OSError as err:
220         error(strerror(err.errno))
224 # Boilerplate
227 if __name__ == '__main__':
228     main()