#! /usr/bin/env python3 from __future__ import print_function import sys, os, string, re, signal, errno # redefine readline to pass invalidly encoded characters unchanged, if possible if hasattr(sys.stdin, 'reconfigure') and hasattr(sys.stdout, 'reconfigure'): # at least python3.7 sys.stdin.reconfigure(errors='surrogateescape') sys.stdout.reconfigure(errors='surrogateescape') myreadline = sys.stdin.readline myprint = print else: if hasattr(sys.stdin, 'buffer'): # python3 buffer_reader = sys.stdin.buffer else: buffer_reader = sys.stdin def myreadline(): for line in buffer_reader: try: decoded = line.decode('utf-8', errors='surrogateescape') except (UnicodeDecodeError, LookupError): decoded = line.decode('utf-8', errors='ignore') return decoded return '' def myprint(x): try: print(x) except UnicodeEncodeError: print(x.encode('utf-8', errors='replace').decode('utf-8')) #some default definitions colours = { 'none' : "", 'default' : "\033[0m", 'bold' : "\033[1m", 'underline' : "\033[4m", 'blink' : "\033[5m", 'reverse' : "\033[7m", 'concealed' : "\033[8m", 'black' : "\033[30m", 'red' : "\033[31m", 'green' : "\033[32m", 'yellow' : "\033[33m", 'blue' : "\033[34m", 'magenta' : "\033[35m", 'cyan' : "\033[36m", 'white' : "\033[37m", 'on_black' : "\033[40m", 'on_red' : "\033[41m", 'on_green' : "\033[42m", 'on_yellow' : "\033[43m", 'on_blue' : "\033[44m", 'on_magenta' : "\033[45m", 'on_cyan' : "\033[46m", 'on_white' : "\033[47m", 'beep' : "\007", 'previous' : "prev", 'unchanged' : "unchanged", # non-standard attributes, supported by some terminals 'dark' : "\033[2m", 'italic' : "\033[3m", 'rapidblink' : "\033[6m", 'strikethrough': "\033[9m", # aixterm bright color codes # prefixed with standard ANSI codes for graceful failure 'bright_black' : "\033[30;90m", 'bright_red' : "\033[31;91m", 'bright_green' : "\033[32;92m", 'bright_yellow' : "\033[33;93m", 'bright_blue' : "\033[34;94m", 'bright_magenta' : "\033[35;95m", 'bright_cyan' : "\033[36;96m", 'bright_white' : "\033[37;97m", 'on_bright_black' : "\033[40;100m", 'on_bright_red' : "\033[41;101m", 'on_bright_green' : "\033[42;102m", 'on_bright_yellow' : "\033[43;103m", 'on_bright_blue' : "\033[44;104m", 'on_bright_magenta' : "\033[45;105m", 'on_bright_cyan' : "\033[46;106m", 'on_bright_white' : "\033[47;107m", } # ignore ctrl C - this is not ideal for standalone grcat, but # enables propagating SIGINT to the other subprocess in grc signal.signal(signal.SIGINT, signal.SIG_IGN) def add2list(clist, m, patterncolour): for group in range(0, len(m.groups()) +1): if group < len(patterncolour): clist.append((m.start(group), m.end(group), patterncolour[group])) else: clist.append((m.start(group), m.end(group), patterncolour[0])) def get_colour(x): if x in colours: return colours[x] elif len(x)>=2 and x[0]=='"' and x[-1]=='"': return eval(x) else: raise ValueError('Bad colour specified: '+x) home = [] conffile = None xdg_config = os.environ.get('XDG_CONFIG_HOME') xdg_data = os.environ.get('XDG_DATA_HOME') home = os.environ.get('HOME') if home and not xdg_config: xdg_config = home + '/.config' if home and not xdg_data: xdg_data = home + '/.local/share' conffilepath = [""] if xdg_data: conffilepath += [xdg_data + '/grc/'] if xdg_config: conffilepath += [xdg_config + '/grc/'] if home: conffilepath += [home + '/.grc/'] conffilepath += ['/usr/local/share/grc/', '/usr/share/grc/'] if len(sys.argv) != 2: sys.stderr.write("You are not supposed to call grcat directly, but the usage is: grcat conffile\n") sys.exit(1) conffile_arg = sys.argv[1] # tentative conffile for i in conffilepath: # test if conffile exists, it can be also a pipe if os.path.exists(i+conffile_arg) and not os.path.isdir(i+conffile_arg): conffile = i+conffile_arg break if not conffile: sys.stderr.write("config file [%s] not found\n" % sys.argv[1]) sys.exit(1) regexplist = [] f = open(conffile, "r") is_last = 0 split = str.split lower = str.lower letters = string.ascii_letters while not is_last: ll = {'count':"more"} while 1: l = f.readline() if l == "": is_last = 1 break if l[0] == "#" or l[0] == '\012': continue if not l[0] in letters: break fields = split(l.rstrip('\r\n'), "=", 1) if len(fields) != 2: sys.stderr.write('Error in configuration, I expect keyword=value line\n') sys.stderr.write('But I got instead:\n') sys.stderr.write(repr(l)) sys.stderr.write('\n') sys.exit(1) keyword, value = fields keyword = lower(keyword) if keyword in ('colors', 'colour', 'color'): keyword = 'colours' if not keyword in ["regexp", "colours", "count", "command", "skip", "replace", "concat"]: raise ValueError("Invalid keyword") ll[keyword] = value # Split string into one string per regex group # e.g. split "brown bold, red" into "brown bold" and # "red" #colstrings = [] #for colgroup in split(ll['colours'], ','): # colourlist = split(colgroup) # c = "" # for i in colourlist : # c = c + colours[i] # colstrings.append(c) # do not try to understand the optimized form below :-) if 'colours' in ll: colstrings = list( [''.join([get_colour(x) for x in split(colgroup)]) for colgroup in split(ll['colours'], ',')] ) ll['colours'] = colstrings cs = ll['count'] if 'regexp' in ll: ll['regexp'] = re.compile(ll['regexp']).search regexplist.append(ll) prevcolour = colours['default'] prevcount = "more" blockflag = 0 while 1: line = myreadline() if line == "" : break if line[-1] in '\r\n': line = line[:-1] clist = [] skip = 0 for pattern in regexplist: pos = 0 currcount = pattern['count'] was_replace = 0 # watch replacements, replace regexp only one per line, to avoid infinte loops if the replacement matches the regexp again while 1: m = pattern['regexp'](line, pos) if m: if 'replace' in pattern: if was_replace: break line = re.sub(m.re, pattern['replace'], line) was_replace = 1 if 'colours' in pattern: if currcount == "block": blockflag = 1 blockcolour = pattern['colours'][0] currcount = "stop" break elif currcount == "unblock": blockflag = 0 blockcolour = colours['default'] currcount = "stop" add2list(clist, m, pattern['colours']) if currcount == "previous": currcount = prevcount if currcount == "stop": break if currcount == "more": prevcount = "more" newpos = m.end(0) # special case, if the regexp matched but did not consume anything, # advance the position by 1 to escape endless loop if newpos == pos: pos += 1 else: pos = newpos else: prevcount = "once" pos = len(line) if 'concat' in pattern: with open(pattern['concat'], 'a') as f : f.write(line + '\n') if 'colours' not in pattern: break if 'command' in pattern: os.system(pattern['command']) if 'colours' not in pattern: break if 'skip' in pattern: skip = pattern['skip'] in ("yes", "1", "true") if 'colours' not in pattern: break else: break if m and currcount == "stop": prevcount = "stop" break if len(clist) == 0: prevcolour = colours['default'] first_char = 0 last_char = 0 length_line = len(line) if blockflag == 0: cline = (length_line+1)*[colours['default']] for i in clist: # each position in the string has its own colour if i[2] == "prev": cline[i[0]:i[1]] = [colours['default']+prevcolour]*(i[1]-i[0]) elif i[2] != "unchanged": cline[i[0]:i[1]] = [colours['default']+i[2]]*(i[1]-i[0]) if i[0] == 0: first_char = 1 if i[2] != "prev": prevcolour = i[2] if i[1] == length_line: last_char = 1 if first_char == 0 or last_char == 0: prevcolour = colours['default'] else: cline = (length_line+1)*[blockcolour] nline = "" clineprev = "" if not skip: for i in range(len(line)): if cline[i] == clineprev: nline = nline + line[i] else: nline = nline + cline[i] + line[i] clineprev = cline[i] nline = nline + colours['default'] try: myprint(nline) except IOError as e: if e.errno == errno.EPIPE: break else: raise