File: ecoli.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2020-2025 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 from re import compile, Pattern 27 from sys import argv, exit, stderr, stdin, stdout 28 from typing import List, Tuple 29 30 31 info = ''' 32 ecoli [regex/style pairs...] 33 34 Expressions COloring LInes tries to match each line to the regexes given, 35 coloring/styling with the named-style associated to the first match, if 36 any. 37 38 Lines not matching any regex stay verbatim. 39 40 The colors/styles available are: 41 blue 42 bold 43 gray 44 green 45 inverse 46 magenta 47 orange 48 purple 49 red 50 underline 51 52 Some style aliases are: 53 b blue 54 g green 55 m magenta 56 o orange 57 p purple 58 r red 59 u underline 60 hi inverse (highlight) 61 ''' 62 63 # no args or a leading help-option arg means show the help message and quit 64 if len(argv) == 1 or argv[1] in ('-h', '--h', '-help', '--help'): 65 print(info.strip(), file=stderr) 66 exit(0) 67 68 69 def fail(msg, code: int = 1) -> None: 70 'Show the error message given, and quit the app right away.' 71 print(f'\x1b[31m{msg}\x1b[0m', file=stderr) 72 exit(code) 73 74 75 def style_line(w, line: str, pairs: List[Tuple[Pattern, str]]) -> None: 76 'Does what it says, emitting output to stdout.' 77 78 for (expr, style) in pairs: 79 # regexes can match anywhere on the line 80 if expr.search(line) != None: 81 w.write(style) 82 w.write(line) 83 w.write('\x1b[0m\n') 84 return 85 86 # emit the line as is, when no regex matches it 87 w.write(line) 88 w.write('\n') 89 90 91 # names_aliases normalizes lookup keys for table names2styles 92 names_aliases = { 93 'b': 'blue', 94 'g': 'green', 95 'm': 'magenta', 96 'o': 'orange', 97 'p': 'purple', 98 'r': 'red', 99 'u': 'underline', 100 101 'bb': 'bblue', 102 'bg': 'bgreen', 103 'bm': 'bmagenta', 104 'bo': 'borange', 105 'bp': 'bpurple', 106 'br': 'bred', 107 'bu': 'bunderline', 108 109 'bb': 'bblue', 110 'gb': 'bgreen', 111 'mb': 'bmagenta', 112 'ob': 'borange', 113 'pb': 'bpurple', 114 'rb': 'bred', 115 'ub': 'bunderline', 116 117 'hi': 'inverse', 118 'inv': 'inverse', 119 'mag': 'magenta', 120 121 'flip': 'inverse', 122 'swap': 'inverse', 123 124 'reset': 'plain', 125 'highlight': 'inverse', 126 'hilite': 'inverse', 127 'invert': 'inverse', 128 'inverted': 'inverse', 129 'swapped': 'inverse', 130 131 'blueback': 'bblue', 132 'grayback': 'bgray', 133 'greenback': 'bgreen', 134 'magback': 'bmagenta', 135 'magentaback': 'bmagenta', 136 'orangeback': 'borange', 137 'purpleback': 'bpurple', 138 'redback': 'bred', 139 140 'bgblue': 'bblue', 141 'bggray': 'bgray', 142 'bggreen': 'bgreen', 143 'bgmag': 'bmagenta', 144 'bgmagenta': 'bmagenta', 145 'bgorange': 'borange', 146 'bgpurple': 'bpurple', 147 'bgred': 'bred', 148 149 'bluebg': 'bblue', 150 'graybg': 'bgray', 151 'greenbg': 'bgreen', 152 'magbg': 'bmagenta', 153 'magentabg': 'bmagenta', 154 'orangebg': 'borange', 155 'purplebg': 'bpurple', 156 'redbg': 'bred', 157 158 'backblue': 'bblue', 159 'backgray': 'bgray', 160 'backgreen': 'bgreen', 161 'backmag': 'bmagenta', 162 'backmagenta': 'bmagenta', 163 'backorange': 'borange', 164 'backpurple': 'bpurple', 165 'backred': 'bred', 166 } 167 168 # names2styles matches color/style names to their ANSI-style strings 169 names2styles = { 170 'blue': '\x1b[38;2;0;95;215m', 171 'bold': '\x1b[1m', 172 'gray': '\x1b[38;2;168;168;168m', 173 'green': '\x1b[38;2;0;135;95m', 174 'inverse': '\x1b[7m', 175 'magenta': '\x1b[38;2;215;0;255m', 176 'orange': '\x1b[38;2;215;95;0m', 177 'plain': '\x1b[0m', 178 'purple': '\x1b[38;2;135;95;255m', 179 'red': '\x1b[38;2;204;0;0m', 180 'underline': '\x1b[4m', 181 182 'bblue': '\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m', 183 'bgray': '\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m', 184 'bgreen': '\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m', 185 'bmagenta': '\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m', 186 'borange': '\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m', 187 'bpurple': '\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m', 188 'bred': '\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m', 189 } 190 191 192 # ensure an even number of args, after any leading options 193 if len(argv) % 2 != 1: 194 pre = 'expected an even number of args as regex/style pairs' 195 fail(f'{pre}, but got {len(argv) - 1} instead') 196 197 # make regex/ANSI-style pairs which are directly usable 198 pairs: List[Tuple[Pattern, str]] = [] 199 start_args = 1 200 while start_args + 1 < len(argv): 201 # compile regex 202 try: 203 expr = compile(argv[start_args + 0]) 204 except Exception as e: 205 fail(e) 206 207 # lookup style name 208 name = argv[start_args + 1] 209 if name in names_aliases: 210 name = names_aliases[name] 211 if not name in names2styles: 212 fail(f'style named {name} not supported') 213 214 # remember both for later 215 pairs.append((expr, names2styles[name])) 216 # previous check ensures args has an even number of items 217 start_args += 2 218 219 try: 220 for line in stdin: 221 # ignore trailing carriage-returns and/or line-feeds in input lines 222 style_line(stdout, line.rstrip('\r\n').rstrip('\n'), pairs) 223 except BrokenPipeError: 224 # quit quietly, instead of showing a confusing error message 225 stderr.close() 226 except KeyboardInterrupt: 227 exit(2)