bash-color-grc/grcat

313 lines
11 KiB
Python
Executable File

#! /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