File: ecoli.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 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 [options...] [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;5;26m',
 171     'bold': '\x1b[1m',
 172     'gray': '\x1b[38;5;248m',
 173     'green': '\x1b[38;5;29m',
 174     'inverse': '\x1b[7m',
 175     'magenta': '\x1b[38;5;165m',
 176     'orange': '\x1b[38;5;166m',
 177     'plain': '\x1b[0m',
 178     'purple': '\x1b[38;5;99m',
 179     'red': '\x1b[31m',
 180     'underline': '\x1b[4m',
 181 
 182     'bblue': '\x1b[48;5;26m\x1b[38;5;15m',
 183     'bgray': '\x1b[48;5;248m\x1b[38;5;15m',
 184     'bgreen': '\x1b[48;5;29m\x1b[38;5;15m',
 185     'bmagenta': '\x1b[48;5;165m\x1b[38;5;15m',
 186     'borange': '\x1b[48;5;166m\x1b[38;5;15m',
 187     'bpurple': '\x1b[48;5;99m\x1b[38;5;15m',
 188     'bred': '\x1b[41m\x1b[38;5;15m',
 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.flush()
 226     stderr.close()
 227 except KeyboardInterrupt:
 228     # quit quietly, instead of showing a confusing error message
 229     exit(2)