#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2020-2025 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from re import compile, IGNORECASE, Pattern from sys import argv, exit, stderr, stdin, stdout from typing import List, Tuple info = ''' iecoli [regex/style pairs...] Insensitive Expressions COloring LInes tries to case-insensitively match each line to the regexes given, coloring/styling with the named-style associated to the first match, if any. Lines not matching any regex stay verbatim. The colors/styles available are: blue bold gray green inverse magenta orange purple red underline Some style aliases are: b blue g green m magenta o orange p purple r red u underline hi inverse (highlight) ''' # no args or a leading help-option arg means show the help message and quit if len(argv) == 1 or argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip(), file=stderr) exit(0) def fail(msg, code: int = 1) -> None: 'Show the error message given, and quit the app right away.' print(f'\x1b[31m{msg}\x1b[0m', file=stderr) exit(code) def style_line(w, line: str, pairs: List[Tuple[Pattern, str]]) -> None: 'Does what it says, emitting output to stdout.' for (expr, style) in pairs: # regexes can match anywhere on the line if expr.search(line) != None: w.write(style) w.write(line) w.write('\x1b[0m\n') return # emit the line as is, when no regex matches it w.write(line) w.write('\n') # names_aliases normalizes lookup keys for table names2styles names_aliases = { 'b': 'blue', 'g': 'green', 'm': 'magenta', 'o': 'orange', 'p': 'purple', 'r': 'red', 'u': 'underline', 'bb': 'bblue', 'bg': 'bgreen', 'bm': 'bmagenta', 'bo': 'borange', 'bp': 'bpurple', 'br': 'bred', 'bu': 'bunderline', 'bb': 'bblue', 'gb': 'bgreen', 'mb': 'bmagenta', 'ob': 'borange', 'pb': 'bpurple', 'rb': 'bred', 'ub': 'bunderline', 'hi': 'inverse', 'inv': 'inverse', 'mag': 'magenta', 'flip': 'inverse', 'swap': 'inverse', 'reset': 'plain', 'highlight': 'inverse', 'hilite': 'inverse', 'invert': 'inverse', 'inverted': 'inverse', 'swapped': 'inverse', 'blueback': 'bblue', 'grayback': 'bgray', 'greenback': 'bgreen', 'magback': 'bmagenta', 'magentaback': 'bmagenta', 'orangeback': 'borange', 'purpleback': 'bpurple', 'redback': 'bred', 'bgblue': 'bblue', 'bggray': 'bgray', 'bggreen': 'bgreen', 'bgmag': 'bmagenta', 'bgmagenta': 'bmagenta', 'bgorange': 'borange', 'bgpurple': 'bpurple', 'bgred': 'bred', 'bluebg': 'bblue', 'graybg': 'bgray', 'greenbg': 'bgreen', 'magbg': 'bmagenta', 'magentabg': 'bmagenta', 'orangebg': 'borange', 'purplebg': 'bpurple', 'redbg': 'bred', 'backblue': 'bblue', 'backgray': 'bgray', 'backgreen': 'bgreen', 'backmag': 'bmagenta', 'backmagenta': 'bmagenta', 'backorange': 'borange', 'backpurple': 'bpurple', 'backred': 'bred', } # names2styles matches color/style names to their ANSI-style strings names2styles = { 'blue': '\x1b[38;2;0;95;215m', 'bold': '\x1b[1m', 'gray': '\x1b[38;2;168;168;168m', 'green': '\x1b[38;2;0;135;95m', 'inverse': '\x1b[7m', 'magenta': '\x1b[38;2;215;0;255m', 'orange': '\x1b[38;2;215;95;0m', 'plain': '\x1b[0m', 'purple': '\x1b[38;2;135;95;255m', 'red': '\x1b[38;2;204;0;0m', 'underline': '\x1b[4m', 'bblue': '\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m', 'bgray': '\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m', 'bgreen': '\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m', 'bmagenta': '\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m', 'borange': '\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m', 'bpurple': '\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m', 'bred': '\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m', } # ensure an even number of args, after any leading options if len(argv) % 2 != 1: pre = 'expected an even number of args as regex/style pairs' fail(f'{pre}, but got {len(argv) - 1} instead') # make regex/ANSI-style pairs which are directly usable pairs: List[Tuple[Pattern, str]] = [] start_args = 1 while start_args + 1 < len(argv): # compile regex try: expr = compile(argv[start_args + 0], flags=IGNORECASE) except Exception as e: fail(e) # lookup style name name = argv[start_args + 1] if name in names_aliases: name = names_aliases[name] if not name in names2styles: fail(f'style named {name} not supported') # remember both for later pairs.append((expr, names2styles[name])) # previous check ensures args has an even number of items start_args += 2 try: for line in stdin: # ignore trailing carriage-returns and/or line-feeds in input lines style_line(stdout, line.rstrip('\r\n').rstrip('\n'), pairs) except BrokenPipeError: # quit quietly, instead of showing a confusing error message stderr.close() except KeyboardInterrupt: exit(2)