File: iecoli.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 # iecoli [options...] [regex/style pairs...] 27 # 28 # Insensitive Expressions COloring LInes tries to case-insensitively match 29 # each line to the regexes given, coloring/styling with the named-style 30 # associated to the first match, if any. 31 # 32 # Lines not matching any regex stay verbatim. 33 # 34 # The colors/styles available are: 35 # blue 36 # bold 37 # gray 38 # green 39 # inverse 40 # magenta 41 # orange 42 # purple 43 # red 44 # underline 45 # 46 # Some style aliases are: 47 # b blue 48 # g green 49 # m magenta 50 # o orange 51 # p purple 52 # r red 53 # u underline 54 # hi inverse (highlight) 55 56 57 from re import compile, IGNORECASE, Pattern 58 from sys import argv, exit, stderr, stdin, stdout 59 from typing import List, Tuple 60 61 62 # info is the message shown when the script isn't given any argument, or 63 # when the leading argument is one of the standard cmd-line help options 64 info = ''' 65 iecoli [options...] [regex/style pairs...] 66 67 Insensitive Expressions COloring LInes tries to case-insensitively match 68 each line to the regexes given, coloring/styling with the named-style 69 associated to the first match, if any. 70 71 Lines not matching any regex stay verbatim. 72 73 The colors/styles available are: 74 blue 75 bold 76 gray 77 green 78 inverse 79 magenta 80 orange 81 purple 82 red 83 underline 84 85 Some style aliases are: 86 b blue 87 g green 88 m magenta 89 o orange 90 p purple 91 r red 92 u underline 93 hi inverse (highlight) 94 '''.strip() 95 96 97 def fail(msg, code: int = 1) -> None: 98 '''Show the error message given, and quit the app right away.''' 99 print(f'\x1b[31m{msg}\x1b[0m', file=stderr) 100 exit(code) 101 102 103 def style_line(w, line: str, pairs: List[Tuple[Pattern, str]]) -> None: 104 '''Does what it says, emitting output to stdout.''' 105 106 for (expr, style) in pairs: 107 # regexes can match anywhere on the line 108 if expr.search(line) != None: 109 w.write(style) 110 w.write(line) 111 w.write('\x1b[0m\n') 112 return 113 114 # emit the line as is, when no regex matches it 115 w.write(line) 116 w.write('\n') 117 118 119 # names_aliases normalizes lookup keys for table names2styles 120 names_aliases = { 121 'b': 'blue', 122 'g': 'green', 123 'm': 'magenta', 124 'o': 'orange', 125 'p': 'purple', 126 'r': 'red', 127 'u': 'underline', 128 129 'hi': 'inverse', 130 'inv': 'inverse', 131 'mag': 'magenta', 132 133 'flip': 'inverse', 134 'swap': 'inverse', 135 136 'reset': 'plain', 137 'highlight': 'inverse', 138 'hilite': 'inverse', 139 'invert': 'inverse', 140 'inverted': 'inverse', 141 'swapped': 'inverse', 142 } 143 144 # names2styles matches color/style names to their ANSI-style strings 145 names2styles = { 146 'blue': '\x1b[38;5;26m', 147 'bold': '\x1b[1m', 148 'gray': '\x1b[38;5;249m', 149 'green': '\x1b[38;5;29m', 150 'inverse': '\x1b[7m', 151 'magenta': '\x1b[38;5;165m', 152 'orange': '\x1b[38;5;166m', 153 'plain': '\x1b[0m', 154 'purple': '\x1b[38;5;99m', 155 'red': '\x1b[31m', 156 'underline': '\x1b[4m', 157 } 158 159 160 # no args or a leading help-option arg means show the help message and quit 161 if len(argv) == 1 or argv[1] in ('-h', '--h', '-help', '--help'): 162 print(info, file=stderr) 163 exit(0) 164 165 # ensure an even number of args, after any leading options 166 if len(argv) % 2 != 1: 167 pre = 'expected an even number of args as regex/style pairs' 168 fail(f'{pre}, but got {len(argv) - 1} instead') 169 170 # make regex/ANSI-style pairs which are directly usable 171 pairs: List[Tuple[Pattern, str]] = [] 172 start_args = 1 173 while start_args + 1 < len(argv): 174 # compile regex 175 try: 176 expr = compile(argv[start_args + 0], flags=IGNORECASE) 177 except Exception as e: 178 fail(e) 179 180 # lookup style name 181 name = argv[start_args + 1] 182 if name in names_aliases: 183 name = names_aliases[name] 184 if not name in names2styles: 185 fail(f'style named {name} not supported') 186 187 # remember both for later 188 pairs.append((expr, names2styles[name])) 189 # previous check ensures args has an even number of items 190 start_args += 2 191 192 try: 193 stdout.reconfigure(newline='\n', encoding='utf-8') 194 for line in stdin: 195 # ignore trailing carriage-returns and/or line-feeds in input lines 196 style_line(stdout, line.rstrip('\r\n').rstrip('\n'), pairs) 197 except BrokenPipeError: 198 # quit quietly, instead of showing a confusing error message 199 stderr.flush() 200 stderr.close() 201 except KeyboardInterrupt: 202 # quit quietly, instead of showing a confusing error message 203 stderr.flush() 204 stderr.close() 205 exit(2)