macrop.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. #!/usr/bin/env python3
  2. """Simple macro processor"""
  3. #
  4. # Copyright 2024 Odin Kroeger
  5. #
  6. # This program is free software: you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation, either version 3 of
  9. # the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ALL WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public
  17. # License along with this program. If not, see
  18. # <https://www.gnu.org/licenses/>.
  19. #
  20. #
  21. # Modules
  22. #
  23. from contextlib import suppress
  24. from filecmp import cmp
  25. from getopt import getopt, GetoptError
  26. from os import remove, rename, strerror
  27. from os.path import dirname, basename, commonprefix, exists, join
  28. from shutil import copy
  29. from string import Template
  30. # pylint: disable=redefined-builtin
  31. from sys import argv, exit, stderr
  32. from typing import Callable, NoReturn
  33. import logging
  34. import re
  35. import sievemgr as mod
  36. #
  37. # Metadata
  38. #
  39. __author__ = 'Odin Kroeger'
  40. __copyright__ = '2024 Odin Kroeger'
  41. __version__ = '0.1'
  42. #
  43. # Functions
  44. #
  45. def error(*args, status: int = 1, **kwargs) -> NoReturn:
  46. """Log an err and :func:`exit <sys.exit>` with `status`.
  47. Arguments:
  48. args: Positional arguments for :func:`logging.error`.
  49. status: Exit status.
  50. kwargs: Keyword arguments for :func:`logging.error`.
  51. """
  52. logging.error(*args, **kwargs)
  53. exit(status)
  54. def showhelp(func: Callable) -> NoReturn:
  55. """Print the docstring of `func` and :func:`exit <sys.exit>`."""
  56. assert func.__doc__
  57. lines = func.__doc__.splitlines()
  58. indented = re.compile(r'\s+').match
  59. prefix = commonprefix(list(filter(indented, lines)))
  60. for line in lines[:-1]:
  61. print(line.removeprefix(prefix))
  62. exit()
  63. def showversion() -> NoReturn:
  64. """Print version to standard output and exit."""
  65. print(f"macrop {__version__}\nCopyright {__copyright__}")
  66. exit()
  67. #
  68. # Main
  69. #
  70. def main() -> NoReturn:
  71. """macrop - replace variables with dunder globals
  72. Usage: macrop template output
  73. Options:
  74. -V Show version information.
  75. -h Show this help screen.
  76. """
  77. progname = basename(argv[0])
  78. logging.basicConfig(format=f'{progname}: %(message)s')
  79. try:
  80. opts, args = getopt(argv[1:], 'hV', ['help', 'version'])
  81. except GetoptError as err:
  82. error(err, status=2)
  83. for opt, _ in opts:
  84. if opt in ('-h', '--help'):
  85. showhelp(main)
  86. if opt in ('-V', '--version'):
  87. showversion()
  88. try:
  89. source, target = args
  90. except ValueError:
  91. print(f'usage: {progname} [-h] source target', file=stderr)
  92. exit(2)
  93. swap = join(dirname(source), '.' + basename(source) + '.swp')
  94. macros = {k: v for k, v in mod.__dict__.items()
  95. if (re.fullmatch(r'__\w+__', k, re.A | re.I)
  96. and isinstance(v, (int, str)))}
  97. try:
  98. with open(swap, 'w') as swapfile:
  99. with open(source) as sourcefile:
  100. for line in sourcefile:
  101. sub = Template(line).safe_substitute(macros)
  102. print(sub, file=swapfile, end='')
  103. if exists(target):
  104. if cmp(swap, target):
  105. exit(0)
  106. copy(target, target + '.bak')
  107. rename(swap, target)
  108. except FileNotFoundError as err:
  109. error(f'{err.filename}: {strerror(err.errno)}')
  110. except OSError as err:
  111. error(strerror(err.errno))
  112. finally:
  113. with suppress(FileNotFoundError):
  114. remove(swap)
  115. exit(0)
  116. if __name__ == '__main__':
  117. main()