Files
git-alias/git-alias.py
2019-08-13 17:28:34 +02:00

391 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
# coding=utf8
# Iterate over git config file and extract alias help
from argparse import ArgumentParser, FileType
import re
from os import path, linesep as CRLF
from sys import stdout
ARGS = None
def main():
global ARGS
parser = ArgumentParser()
parser.add_argument('FILE', type=FileType('r'),
default=path.expanduser('~/.gitconfig'), nargs='?',
help='git config file (default: %(default)s)')
parser.add_argument('-a', '--all', action='store_true',
help='include hidden aliases')
parser.add_argument('-v', '--verbose', action='count', default=0,
help='print complex commands (-vv: pretty print)')
parser.add_argument('-q', '--quiet', action='count', default=0,
help='print less information (up to -qqqq | -qqvv)')
parser.add_argument('--color', action='store_const', const='+',
help='force color output', dest='color')
parser.add_argument('--no-color', action='store_const', const='-',
help='disable color output', dest='color')
ARGS = parser.parse_args()
colorMap = {'+': True, '-': False, None: stdout.isatty()}
ARGS.color = colorMap[ARGS.color]
AliasConfig(ARGS.FILE).parse()
ARGS.FILE.close()
class AliasConfig(object):
''' Parser for git-config files '''
def __init__(self, file):
super(AliasConfig, self).__init__()
self.file = file
self.dirty = False
self.state = None
self.prevLine = None
self.comments = []
def parse(self):
for line in self.file.readlines():
# A command can be multiline if the line ends with \
if self.state == 'alias' and self.prevLine:
line = line.rstrip()
if line.endswith('\\'):
self.prevLine += '\n' + line[:-1]
else:
self.prevLine += '\n' + line
self.parseAlias(self.prevLine)
self.prevLine = None
continue
else:
line = line.strip()
# Ignore empty lines
# But empty comment will insert a blank line
if len(line) == 0:
continue
if self.parseState(line):
continue
# Check if we need to include another alias file
if self.state == 'include':
self.parseIncludePath(line)
continue
# Ignore parsing for groups other than 'alias'
if self.state != 'alias':
continue
# Add comments to internal array
if self.isComment(line):
continue
# not a config group, not a comment ... must be the alias
if line.endswith('\\'):
self.prevLine = line[:-1]
else:
self.parseAlias(line)
def parseState(self, line):
config = re.search(r'^\[(.+)\]$', line)
if config:
self.state = config.group(1).strip().lower()
return config
def printSection(self, title):
if ARGS.quiet <= 1:
print(Format().section(title.strip(';# \t'), self.dirty))
self.dirty = True
def isComment(self, line):
if line[0] not in ';#':
return False
# If line starts with ## asume this to be a section title
noBlanks = line.replace(' ', '')
if len(noBlanks) > 1 and noBlanks[1] in ';#':
self.printSection(line)
else:
self.comments.append(line[1:].lstrip())
return True
def parseAlias(self, line):
cmd = re.search(r'^(.+?)\s*=\s*([\s\S]+)$', line)
alias = Alias(cmd.group(1).strip(), cmd.group(2).strip())
if alias.parse(self.comments):
print(alias)
self.comments = []
self.dirty = True
def parseIncludePath(self, line):
param = re.search(r'^(.+?)\s*=\s*([\s\S]+)$', line)
if param and param.group(1).strip() == 'path':
path = param.group(2).strip()
if ARGS.quiet <= 1: # if 'quiet == 0', copy self.dirty
print(CRLF + '@include: %s' % path)
with open(path, 'r') as f:
AliasConfig(f).parse()
if ARGS.quiet <= 1:
print('@end: %s' % path + CRLF)
class Alias(object):
''' Object for alias name, command, and helping hints '''
def __init__(self, name, command):
super(Alias, self).__init__()
self.name = name
self.command = AliasCommand(command)
self.usage = None
self.hints = ''
self.ignore = False
def __str__(self):
str = Format().alias(self.name)
if self.usage and ARGS.quiet <= 3:
str += ' ' + Format().usage(self.usage)
if self.command.shouldPrint(self.hints):
str += self.command.__str__()
if ARGS.quiet == 0:
str += self.hints
if ARGS.verbose >= 1 and ARGS.quiet <= 2:
str += CRLF
return str
def addDescription(self, line):
self.hints += Format().indent(line)
def parse(self, commentsList):
''' Process list of comments (all comments above alias) '''
for comment in commentsList:
x = comment.split(':')
ctrl = x[0].strip().lower()
if len(x) < 2 or self.ctrlSequence(ctrl, ':'.join(x[1:]).strip()):
# Allow user to hide individual comments with '#!#'
if not comment.startswith('!#'):
self.addDescription(comment)
if self.ignore and not ARGS.all:
return False
return True
def ctrlSequence(self, instruction, tail):
''' Parse lines with format `%s: %s` '''
# Auto-detect urls
if instruction in ['http', 'https']:
tail = instruction + ':' + tail
instruction = 'link'
# Append usage (in red) to command (ignoring first word)
if instruction in ['usage', 'use']:
self.usage = re.sub(r'^%s\s*' % self.name, '', tail, flags=re.I)
# Print url (in light gray)
elif instruction in ['see', 'link', 'url', 'web']:
self.addDescription(Format().link(tail))
# Some program specific controls
elif instruction == '!':
self.lintInstruction(tail.lower())
else:
return True
return False
def lintInstruction(self, instruction):
''' Parse lines with format `!: %s` '''
for _cmd_ in [x.strip() for x in instruction.split(',')]:
if _cmd_ == 'ignore':
self.ignore = True
elif re.match('^show( command| cmd)?$', _cmd_):
self.command.show = True
elif re.match('^hide|(hide|not?) (command|cmd)$', _cmd_):
self.command.show = False
elif re.match('^(single ?|in)line$', _cmd_):
self.command.inline = True
elif re.match('^(new ?|multi ?|not? (single ?|in))line$', _cmd_):
self.command.inline = False
elif re.match('^prett(if)?y', _cmd_):
self.command.prettify = True
class AliasCommand(object):
'''
Very basic bash parser.
Inserts new line for: ';', '{', '}', ' && ' and ' | '
Escapes: \"
'''
def __init__(self, txt):
super(AliasCommand, self).__init__()
self.input = txt
# linting
self.inline = len(txt) < 42 # not self.isComplex()
self.show = None
self.prettify = False
# result parsing
self.skip = 0
self.indentation = 0
self.tempIndent = False
self.newline = False
self.escapeQuotes = False
def __str__(self):
cmd = self.input
if ARGS.verbose >= 2:
self.inline = False
if self.isComplex() and (self.prettify or ARGS.verbose >= 2):
cmd = self.parse()
elif ARGS.verbose == 1 and not self.inline:
cmd = Format().fx([RED, FAINT], cmd)
return Format().command(cmd, self.inline)
def shouldPrint(self, hasHints=False):
if (self.show is False and not ARGS.all) or ARGS.quiet >= 3:
return False
# Print complex command only if user added no comment to describe it
# Otherwise, its most likely too complex to display
if not self.inline:
if hasHints and not self.show and ARGS.verbose == 0:
return False
return True
def isComplex(self):
return self.input.lstrip('"').startswith('!')
def preprocessInput(self, text):
if text.startswith('!'):
text = text[1:].lstrip()
if text.startswith('"'):
text = text[1:].lstrip('! \t')
self.escapeQuotes = True
return text
def append(self, char):
if self.newline and char in ' \t\n\r':
return
if self.newline:
self.newline = False
self.result += Format().indent(' ' * 4 * self.indentation)
if self.tempIndent:
self.result += Format().fx([BLACK, FAINT], '')
self.tempIndent = False
self.result += char
def parse(self):
raw = self.preprocessInput(self.input)
self.result = ''
for i, char in enumerate(raw):
if self.skip > 0:
self.skip -= 1
continue
if char == '\\':
if self.parseCharEscape(raw[i + 1]):
continue
elif char == '$':
if self.parseBashVariable(raw[i + 1:]):
continue
elif self.escapeQuotes and char == '"':
self.escapeQuotes = False
continue
# Insert new lines after '{', '}', and ';'
elif char == '}' and raw[i - 1] != '{':
self.indentation -= 1
self.newline = True
self.append(char)
if char == '{' and raw[i + 1] != '}':
self.indentation += 1
self.newline = True
elif char == ';':
self.newline = True
# Insert new line for ' && ' and ' | '
elif char in '&|':
self.parseBashPipes(raw[i - 2:i + 2])
return self.result
def parseBashPipes(self, text):
if text == ' && ' or text[1:] == ' | ':
self.tempIndent = True
self.newline = True
def parseBashVariable(self, text):
c = text[0]
var = None
if c in '0123456789!$?#*-@':
var = c
elif c in '{':
match = re.search(r'^.*?[}]', text)
var = match.group(0)
else:
match = re.search(r'^[a-zA-Z_][a-zA-Z_0-9]*', text)
if match:
var = match.group(0)
if var:
# Highlight input variables
self.result += Format().inlineVariable('$' + var)
self.skip = len(var)
return self.skip > 0
def parseCharEscape(self, following):
if self.escapeQuotes and following == '"':
self.result += following
self.skip = 1
return self.skip > 0
class Format(object):
''' Abstraction for structuring output '''
def __init__(self):
super(Format, self).__init__()
def section(self, text, newline=True):
head = self.fx([BLUE, BOLD, UNDERLINE], text, alt='=== %s ===')
return (CRLF if newline else '') + head
def alias(self, text):
if ARGS.quiet >= 2:
return self.fx([RED, BOLD], text)
return ' ' + self.fx([RED, BOLD], text, alt='+ %s')
def usage(self, text):
return self.fx([RED], text)
def command(self, text, inline=True):
return ('' if inline else self.indent()) + text
def link(self, text):
return '@: ' + self.fx([BLACK, FAINT], text)
def indent(self, text=''):
return CRLF + ' ' + text
def inlineVariable(self, text):
return self.fx([YELLOW, BOLD], text)
def fx(self, params, text, alt='%s'):
''' if --no-color use alternative format '''
if ARGS.color:
left = '\x1b[' + ';'.join(params) + 'm'
right = '\x1b[0m'
return left + text.replace(right, right + left) + right
else:
return alt % text
BLACK = '30'
RED = '31'
GREEN = '32'
YELLOW = '33'
BLUE = '34'
MAGENTA = '35'
CYAN = '36'
WHITE = '37'
BOLD = '01'
FAINT = '02'
ITALIC = '03'
UNDERLINE = '04'
BLINKSLOW = '05'
BLINK = '06'
NEGATIVE = '07'
CONCEALED = '08'
if __name__ == '__main__':
main()