diff options
Diffstat (limited to 'src/auto-auto-complete.py')
-rwxr-xr-x | src/auto-auto-complete.py | 841 |
1 files changed, 841 insertions, 0 deletions
diff --git a/src/auto-auto-complete.py b/src/auto-auto-complete.py new file mode 100755 index 0000000..35d8ab4 --- /dev/null +++ b/src/auto-auto-complete.py @@ -0,0 +1,841 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +auto-auto-complete – Autogenerate shell auto-completion scripts + +Copyright © 2012, 2013 Mattias Andrée (maandree@member.fsf.org) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' +import sys + + +''' +Hack to enforce UTF-8 in output (in the future, if you see anypony not using utf-8 in +programs by default, report them to Princess Celestia so she can banish them to the moon) + +@param text:str The text to print (empty string is default) +@param end:str The appendix to the text to print (line breaking is default) +''' +def print(text = '', end = '\n'): + sys.stdout.buffer.write((str(text) + end).encode('utf-8')) + +''' +stderr equivalent to print() + +@param text:str The text to print (empty string is default) +@param end:str The appendix to the text to print (line breaking is default) +''' +def printerr(text = '', end = '\n'): + sys.stderr.buffer.write((str(text) + end).encode('utf-8')) + + +''' +Abort the program + +@param text:str Error message +@return returncode:int The programs return code +''' +def abort(text, returncode = 1): + printerr('\033[01;31m%s\033[00m' % text) + sys.exit(returncode) + + + + +''' +Bracket tree parser +''' +class Parser: + ''' + Parse a code and return a tree + + @param code:str The code to parse + @return :list<↑|str> The root node in the tree + ''' + @staticmethod + def parse(code): + stack = [] + stackptr = -1 + + comment = False + escape = False + quote = None + buf = None + + col = 0 + char = 0 + line = 1 + + for charindex in range(0, len(code)): + c = code[charindex] + if comment: + if c in '\n\r\f': + comment = False + elif escape: + escape = False + if c == 'a': buf += '\a' + elif c == 'b': buf += chr(8) + elif c == 'e': buf += '\033' + elif c == 'f': buf += '\f' + elif c == 'n': buf += '\n' + elif c == 'r': buf += '\r' + elif c == 't': buf += '\t' + elif c == 'v': buf += chr(11) + elif c == '0': buf += '\0' + else: + buf += c + elif c == quote: + quote = None + elif (c in ';#') and (quote is None): + if buf is not None: + stack[stackptr].append(buf) + buf = None + comment = True + elif (c == '(') and (quote is None): + if buf is not None: + stack[stackptr].append(buf) + buf = None + stackptr += 1 + if stackptr == len(stack): + stack.append([]) + else: + stack[stackptr] = [] + elif (c == ')') and (quote is None): + if buf is not None: + stack[stackptr].append(buf) + buf = None + if stackptr == 0: + return stack[0] + stackptr -= 1 + stack[stackptr].append(stack[stackptr + 1]) + elif (c in ' \t\n\r\f') and (quote is None): + if buf is not None: + stack[stackptr].append(buf) + buf = None + else: + if buf is None: + buf = '' + if c == '\\': + escape = True + elif (c in '\'\"') and (quote is None): + quote = c + else: + buf += c + + if c == '\t': + col |= 7 + col += 1 + char += 1 + if c in '\n\r\f': + line += 1 + col = 0 + char = 0 + + abort('premature end of file') + + + ''' + Simplifies a tree + + @param tree:list<↑|str> The tree + ''' + @staticmethod + def simplify(tree): + global variables + program = tree[0] + stack = [tree] + while len(stack) > 0: + node = stack.pop() + new = [] + edited = False + for item in node: + if isinstance(item, list): + if item[0] == 'multiple': + master = item[1] + for slave in item[2:]: + new.append([master] + slave) + edited = True + elif item[0] == 'case': + for alt in item[1:]: + if alt[0] == program: + new.append(alt[1]) + break + edited = True + elif item[0] == 'value': + variable = item[1] + if variable in variables: + for value in variables[variable]: + new.append(value) + else: + if len(item) == 2: + abort('Undefined variable: ' + variable) + for value in item[2:]: + new.append(value) + edited = True + else: + new.append(item) + else: + new.append(item) + if edited: + node[:] = new + for item in node: + if isinstance(item, list): + stack.append(item) + + + +''' +Completion script generator for GNU Bash +''' +class GeneratorBASH: + ''' + Constructor + + @param program:str The command to generate completion for + @param unargumented:list<dict<str, list<str>>> Specification of unargumented options + @param argumented:list<dict<str, list<str>>> Specification of argumented options + @param variadic:list<dict<str, list<str>>> Specification of variadic options + @param suggestion:list<list<↑|str>> Specification of argument suggestions + @param default:dict<str, list<str>>? Specification for optionless arguments + ''' + def __init__(self, program, unargumented, argumented, variadic, suggestion, default): + self.program = program + self.unargumented = unargumented + self.argumented = argumented + self.variadic = variadic + self.suggestion = suggestion + self.default = default + + + ''' + Gets the argument suggesters for each option + + @return :dist<str, str> Map from option to suggester + ''' + def __getSuggesters(self): + suggesters = {} + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if 'suggest' in item: + suggester = item['suggest'] + for option in item['options']: + suggesters[option] = suggester[0] + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if ('suggest' not in item) and ('bind' in item): + bind = item['bind'][0] + if bind in suggesters: + suggester = suggesters[bind] + for option in item['options']: + suggesters[option] = suggester + + return suggesters + + + ''' + Returns the generated code + + @return :str The generated code + ''' + def get(self): + buf = '# bash completion for %s -*- shell-script -*-\n\n' % self.program + buf += '_%s()\n{\n' % self.program + buf += ' local cur prev words cword\n' + buf += ' _init_completion -n = || return\n\n' + + def verb(text): + temp = text + for char in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=/@:\'': + temp = temp.replace(char, '') + if len(temp) == 0: + return text + return '\'' + text.replace('\'', '\'\\\'\'') + '\'' + + def makeexec(functionType, function): + if functionType in ('exec', 'pipe', 'fullpipe', 'cat', 'and', 'or'): + elems = [(' %s ' % makeexec(item[0], item[1:]) if isinstance(item, list) else verb(item)) for item in function] + if functionType == 'exec': + return ' $( %s ) ' % (' '.join(elems)) + if functionType == 'pipe': + return ' ( %s ) ' % (' | '.join(elems)) + if functionType == 'fullpipe': + return ' ( %s ) ' % (' |& '.join(elems)) + if functionType == 'cat': + return ' ( %s ) ' % (' ; '.join(elems)) + if functionType == 'and': + return ' ( %s ) ' % (' && '.join(elems)) + if functionType == 'or': + return ' ( %s ) ' % (' || '.join(elems)) + if functionType in ('params', 'verbatim'): + return ' '.join([verb(item) for item in function]) + return ' '.join([verb(functionType)] + [verb(item) for item in function]) + + def makesuggestion(suggester): + suggestion = ''; + for function in suggester: + functionType = function[0] + function = function[1:] + if functionType == 'verbatim': + suggestion += ' %s' % (' '.join([verb(item) for item in function])) + elif functionType == 'ls': + filter = '' + if len(function) > 1: + filter = ' | grep -v \\/%s\\$ | grep %s\\$' % (function[1], function[1]) + suggestion += ' $(ls -1 --color=no %s%s)' % (function[0], filter) + elif functionType in ('exec', 'pipe', 'fullpipe', 'cat', 'and', 'or'): + suggestion += (' %s' if functionType == 'exec' else ' $(%s)') % makeexec(functionType, function) + elif functionType == 'calc': + expression = [] + for item in function: + if isinstance(item, list): + expression.append(('%s' if item[0] == 'exec' else '$(%s)') % makeexec(item[0], item[1:])) + else: + expression.append(verb(item)) + suggestion += ' $(( %s ))' % (' '.join(expression)) + return '"' + suggestion + '"' + + suggesters = self.__getSuggesters() + suggestFunctions = {} + for function in self.suggestion: + suggestFunctions[function[0]] = function[1:] + + options = [] + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if 'complete' in item: + options += item['complete'] + buf += ' options="%s "' % (' '.join(options)) + if self.default is not None: + defSuggest = self.default['suggest'][0] + if defSuggest is not None: + buf += '%s' % makesuggestion(suggestFunctions[defSuggest]) + buf += '\n' + buf += ' COMPREPLY=( $( compgen -W "$options" -- "$cur" ) )\n\n' + + indenticals = {} + for option in suggesters: + suggester = suggestFunctions[suggesters[option]] + _suggester = str(suggester) + if _suggester not in indenticals: + indenticals[_suggester] = (suggester, [option]) + else: + indenticals[_suggester][1].append(option) + + index = 0 + for _suggester in indenticals: + (suggester, options) = indenticals[_suggester] + conds = [] + for option in options: + conds.append('[ $prev = "%s" ]' % option) + buf += ' %s %s; then\n' % ('if' if index == 0 else 'elif', ' || '.join(conds)) + suggestion = makesuggestion(suggester); + if len(suggestion) > 0: + buf += ' suggestions=%s\n' % suggestion + buf += ' COMPREPLY=( $( compgen -W "$suggestions" -- "$cur" ) )\n' + index += 1 + + if index > 0: + buf += ' fi\n' + + buf += '}\n\ncomplete -o default -F _%s %s\n\n' % (self.program, self.program) + return buf + + + +''' +Completion script generator for fish +''' +class GeneratorFISH: + ''' + Constructor + + @param program:str The command to generate completion for + @param unargumented:list<dict<str, list<str>>> Specification of unargumented options + @param argumented:list<dict<str, list<str>>> Specification of argumented options + @param variadic:list<dict<str, list<str>>> Specification of variadic options + @param suggestion:list<list<↑|str>> Specification of argument suggestions + @param default:dict<str, list<str>>? Specification for optionless arguments + ''' + def __init__(self, program, unargumented, argumented, variadic, suggestion, default): + self.program = program + self.unargumented = unargumented + self.argumented = argumented + self.variadic = variadic + self.suggestion = suggestion + self.default = default + + + ''' + Gets the argument suggesters for each option + + @return :dist<str, str> Map from option to suggester + ''' + def __getSuggesters(self): + suggesters = {} + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if 'suggest' in item: + suggester = item['suggest'] + for option in item['options']: + suggesters[option] = suggester[0] + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if ('suggest' not in item) and ('bind' in item): + bind = item['bind'][0] + if bind in suggesters: + suggester = suggesters[bind] + for option in item['options']: + suggesters[option] = suggester + + return suggesters + + + ''' + Gets the file pattern for each option + + @return :dist<str, list<str>> Map from option to file pattern + ''' + def __getFiles(self): + files = {} + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if 'files' in item: + _files = item['files'] + for option in item['options']: + files[option] = _files + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if ('files' not in item) and ('bind' in item): + bind = item['bind'][0] + if bind in files: + _files = files[bind] + for option in item['options']: + files[option] = _files + + return files + + + ''' + Returns the generated code + + @return :str The generated code + ''' + def get(self): + buf = '# fish completion for %s -*- shell-script -*-\n\n' % self.program + + files = self.__getFiles() + + suggesters = self.__getSuggesters() + suggestFunctions = {} + for function in self.suggestion: + suggestFunctions[function[0]] = function[1:] + + def verb(text): + temp = text + for char in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=/@:\'': + temp = temp.replace(char, '') + if len(temp) == 0: + return text + return '\'' + text.replace('\'', '\'\\\'\'') + '\'' + + def makeexec(functionType, function): + if functionType in ('exec', 'pipe', 'fullpipe', 'cat', 'and', 'or'): + elems = [(' %s ' % makeexec(item[0], item[1:]) if isinstance(item, list) else verb(item)) for item in function] + if functionType == 'exec': + return ' ( %s ) ' % (' '.join(elems)) + if functionType == 'pipe': + return ' ( %s ) ' % (' | '.join(elems)) + if functionType == 'fullpipe': + return ' ( %s ) ' % (' |& '.join(elems)) + if functionType == 'cat': + return ' ( %s ) ' % (' ; '.join(elems)) + if functionType == 'and': + return ' ( %s ) ' % (' && '.join(elems)) + if functionType == 'or': + return ' ( %s ) ' % (' || '.join(elems)) + if functionType in ('params', 'verbatim'): + return ' '.join([verb(item) for item in function]) + return ' '.join([verb(functionType)] + [verb(item) for item in function]) + + index = 0 + for name in suggestFunctions: + suggestion = ''; + for function in suggestFunctions[name]: + functionType = function[0] + function = function[1:] + if functionType == 'verbatim': + suggestion += ' %s' % (' '.join([verb(item) for item in function])) + elif functionType == 'ls': + filter = '' + if len(function) > 1: + filter = ' | grep -v \\/%s\\$ | grep %s\\$' % (function[1], function[1]) + suggestion += ' (ls -1 --color=no %s%s)' % (function[0], filter) + elif functionType in ('exec', 'pipe', 'fullpipe', 'cat', 'and', 'or'): + suggestion += (' %s' if functionType == 'exec' else ' $(%s)') % makeexec(functionType, function) + #elif functionType == 'calc': + # expression = [] + # for item in function: + # if isinstance(item, list): + # expression.append(('%s' if item[0] == 'exec' else '$(%s)') % makeexec(item[0], item[1:])) + # else: + # expression.append(verb(item)) + # suggestion += ' $(( %s ))' % (' '.join(expression)) + if len(suggestion) > 0: + suggestFunctions[name] = '"' + suggestion + '"' + + if self.default is not None: + item = self.default + buf += 'complete --command %s' % self.program + if 'desc' in self.default: + buf += ' --description %s' % verb(' '.join(item['desc'])) + defFiles = self.default['files'] + defSuggest = self.default['suggest'][0] + if defFiles is not None: + if (len(defFiles) == 1) and ('-0' in defFiles): + buf += ' --no-files' + if defSuggest is not None: + buf += ' --arguments %s' % suggestFunctions[defSuggest] + buf += '\n' + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + options = item['options'] + shortopt = [] + longopt = [] + for opt in options: + if opt.startswith('--'): + if ('complete' in item) and (opt in item['complete']): + longopt.append(opt) + elif opt.startswith('-') and (len(opt) == 2): + shortopt.append(opt) + options = shortopt + longopt + if len(longopt) == 0: + continue + buf += 'complete --command %s' % self.program + if 'desc' in item: + buf += ' --description %s' % verb(' '.join(item['desc'])) + if options[0] in files: + if (len(files[options[0]]) == 1) and ('-0' in files[options[0]][0]): + buf += ' --no-files' + if options[0] in suggesters: + buf += ' --arguments %s' % suggestFunctions[suggesters[options[0]]] + if len(shortopt) > 0: buf += ' --short-option %s' % shortopt[0][1:] + if len( longopt) > 0: buf += ' --long-option %s' % longopt[0][2:] + buf += '\n' + + return buf + + + +''' +Completion script generator for zsh +''' +class GeneratorZSH: + ''' + Constructor + + @param program:str The command to generate completion for + @param unargumented:list<dict<str, list<str>>> Specification of unargumented options + @param argumented:list<dict<str, list<str>>> Specification of argumented options + @param variadic:list<dict<str, list<str>>> Specification of variadic options + @param suggestion:list<list<↑|str>> Specification of argument suggestions + @param default:dict<str, list<str>>? Specification for optionless arguments + ''' + def __init__(self, program, unargumented, argumented, variadic, suggestion, default): + self.program = program + self.unargumented = unargumented + self.argumented = argumented + self.variadic = variadic + self.suggestion = suggestion + self.default = default + + + ''' + Gets the argument suggesters for each option + + @return :dist<str, str> Map from option to suggester + ''' + def __getSuggesters(self): + suggesters = {} + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if 'suggest' in item: + suggester = item['suggest'] + for option in item['options']: + suggesters[option] = suggester[0] + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if ('suggest' not in item) and ('bind' in item): + bind = item['bind'][0] + if bind in suggesters: + suggester = suggesters[bind] + for option in item['options']: + suggesters[option] = suggester + + return suggesters + + + ''' + Gets the file pattern for each option + + @return :dist<str, list<str>> Map from option to file pattern + ''' + def __getFiles(self): + files = {} + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if 'files' in item: + _files = item['files'] + for option in item['options']: + files[option] = _files + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + if ('files' not in item) and ('bind' in item): + bind = item['bind'][0] + if bind in files: + _files = files[bind] + for option in item['options']: + files[option] = _files + + return files + + + ''' + Returns the generated code + + @return :str The generated code + ''' + def get(self): + buf = '# zsh completion for %s -*- shell-script -*-\n\n' % self.program + + files = self.__getFiles() + + suggesters = self.__getSuggesters() + suggestFunctions = {} + for function in self.suggestion: + suggestFunctions[function[0]] = function[1:] + + def verb(text): + temp = text + for char in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=/@:\'': + temp = temp.replace(char, '') + if len(temp) == 0: + return text + return '\'' + text.replace('\'', '\'\\\'\'') + '\'' + + def makeexec(functionType, function): + if functionType in ('exec', 'pipe', 'fullpipe', 'cat', 'and', 'or'): + elems = [(' %s ' % makeexec(item[0], item[1:]) if isinstance(item, list) else verb(item)) for item in function] + if functionType == 'exec': + return ' $( %s ) ' % (' '.join(elems)) + if functionType == 'pipe': + return ' ( %s ) ' % (' | '.join(elems)) + if functionType == 'fullpipe': + return ' ( %s ) ' % (' |& '.join(elems)) + if functionType == 'cat': + return ' ( %s ) ' % (' ; '.join(elems)) + if functionType == 'and': + return ' ( %s ) ' % (' && '.join(elems)) + if functionType == 'or': + return ' ( %s ) ' % (' || '.join(elems)) + if functionType in ('params', 'verbatim'): + return ' '.join([verb(item) for item in function]) + return ' '.join([verb(functionType)] + [verb(item) for item in function]) + + index = 0 + for name in suggestFunctions: + suggestion = ''; + for function in suggestFunctions[name]: + functionType = function[0] + function = function[1:] + if functionType == 'verbatim': + suggestion += ' %s ' % (' '.join([verb(item) for item in function])) + elif functionType == 'ls': + filter = '' + if len(function) > 1: + filter = ' | grep -v \\/%s\\$ | grep %s\\$' % (function[1], function[1]) + suggestion += ' $(ls -1 --color=no %s%s) ' % (function[0], filter) + elif functionType in ('exec', 'pipe', 'fullpipe', 'cat', 'and', 'or'): + suggestion += ('%s' if functionType == 'exec' else '$(%s)') % makeexec(functionType, function) + elif functionType == 'calc': + expression = [] + for item in function: + if isinstance(item, list): + expression.append(('%s' if item[0] == 'exec' else '$(%s)') % makeexec(item[0], item[1:])) + else: + expression.append(verb(item)) + suggestion += ' $(( %s )) ' % (' '.join(expression)) + if len(suggestion) > 0: + suggestFunctions[name] = suggestion + + buf += '_opts=(\n' + + for group in (self.unargumented, self.argumented, self.variadic): + for item in group: + options = item['options'] + shortopt = [] + longopt = [] + for opt in options: + if len(opt) > 2: + if ('complete' in item) and (opt in item['complete']): + longopt.append(opt) + elif len(opt) == 2: + shortopt.append(opt) + options = shortopt + longopt + if len(longopt) == 0: + continue + buf += ' \'(%s)\'{%s}' % (' '.join(options), ','.join(options)) + if 'desc' in item: + buf += '"["%s"]"' % verb(' '.join(item['desc'])) + if 'arg' in item: + buf += '":%s"' % verb(' '.join(item['arg'])) + elif options[0] in suggesters: + buf += '": "' + if options[0] in suggesters: + suggestion = suggestFunctions[suggesters[options[0]]] + buf += '":( %s )"' % suggestion + buf += '\n' + + buf += ' )\n\n_arguments "$_opts[@]"\n\n' + return buf + + + +''' +mane! + +@param shell:str Shell to generato completion for +@param output:str Output file +@param source:str Source file +''' +def main(shell, output, source): + with open(source, 'rb') as file: + source = file.read().decode('utf8', 'replace') + source = Parser.parse(source) + Parser.simplify(source) + + program = source[0] + unargumented = [] + argumented = [] + variadic = [] + suggestion = [] + default = None + + for item in source[1:]: + if item[0] == 'unargumented': + unargumented.append(item[1:]); + elif item[0] == 'argumented': + argumented.append(item[1:]); + elif item[0] == 'variadic': + variadic.append(item[1:]); + elif item[0] == 'suggestion': + suggestion.append(item[1:]); + elif item[0] == 'default': + default = item[1:]; + + for (group, not_allowed) in ((unargumented, ['arg', 'suggest', 'files']), (argumented, []), (variadic, [])): + for index in range(0, len(group)): + item = group[index] + map = {} + for elem in item: + if elem[0] not in ('options', 'complete', 'arg', 'suggest', 'files', 'bind', 'desc'): + abort('Unrecognised keyword: ' + elem[0]) + if elem[0] in not_allowed: + abort('Out of context keyword: ' + elem[0]) + map[elem[0]] = elem[1:] + group[index] = map + if default is not None: + map = {} + for elem in default: + if elem[0] not in ('arg', 'suggest', 'files', 'desc'): + abort('Unrecognised keyword: ' + elem[0]) + if elem[0] in ('bind', 'options', 'complete'): + abort('Out of context keyword: ' + elem[0]) + map[elem[0]] = elem[1:] + default = map + + generator = 'Generator' + shell.upper() + generator = globals()[generator] + generator = generator(program, unargumented, argumented, variadic, suggestion, default) + code = generator.get() + + with open(output, 'wb') as file: + file.write(code.encode('utf-8')) + + + +''' +mane! +''' +if __name__ == '__main__': + if len(sys.argv) < 4: + print("USAGE: auto-auto-complete SHELL --output OUTPUT_FILE --source SOURCE_FILE [VARIABLE=VALUE...]") + exit(2) + + shell = None + output = None + source = None + variables = {} + + option = None + aliases = {'-o' : '--output', + '-f' : '--source', '--file' : '--source', + '-s' : '--source'} + + def useopt(option, arg): + global source + global output + global variables + old = None + if option == '--output': old = output; output = arg + elif option == '--source': old = source; source = arg + elif not option.startswith('-'): + if option not in variables: + variables[option] = [] + variables[option].append(arg) + else: + abort('Unrecognised option: ' + option) + if old is not None: + abort('Duplicate option: ' + option) + + for arg in sys.argv[1:]: + if option is not None: + if option in aliases: + option = aliases[option] + useopt(option, arg) + option = None + elif (shell is None) and not arg.startswith('-'): + shell = arg + else: + if '=' in arg: + useopt(arg[:arg.index('=')], arg[arg.index('=') + 1:]) + else: + option = arg + + if output is None: abort('Unused option: --output') + if source is None: abort('Unused option: --source') + + main(shell= shell, output= output, source= source) + |