Signed-off-by: Mattias Andrée <maandree@operamail.com>
This commit is contained in:
Mattias Andrée 2015-04-03 23:03:49 +02:00
parent 2095244c65
commit 5cd37c81b7

View file

@ -5,16 +5,13 @@
############################################################################################### ###############################################################################################
## Shell auto-completion script generator https://www.github.com/maandree/auto-auto-complete ## ## Shell auto-completion script generator https://www.github.com/maandree/auto-auto-complete ##
## Used by build system to make completions for all supported shells. ## ## Used by build system to make completions for all supported shells. ##
## ##
## auto-auto-complete is experimental, therefore, before updating the version of this ##
## make sure that is still work for all shells. ##
############################################################################################### ###############################################################################################
''' '''
auto-auto-complete Autogenerate shell auto-completion scripts auto-auto-complete Autogenerate shell auto-completion scripts
Copyright © 2012 Mattias Andrée (maandree@kth.se) Copyright © 2012, 2013, 2014, 2015 Mattias Andrée (maandree@member.fsf.org)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@ -32,6 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys import sys
def print(text = '', end = '\n'):
''' '''
Hack to enforce UTF-8 in output (in the future, if you see anypony not using utf-8 in 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) programs by default, report them to Princess Celestia so she can banish them to the moon)
@ -39,33 +37,43 @@ programs by default, report them to Princess Celestia so she can banish them to
@param text:str The text to print (empty string is default) @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) @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')) sys.stdout.buffer.write((str(text) + end).encode('utf-8'))
def printerr(text = '', end = '\n'):
''' '''
stderr equivalent to print() stderr equivalent to print()
@param text:str The text to print (empty string is default) @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) @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')) sys.stderr.buffer.write((str(text) + end).encode('utf-8'))
def abort(text, returncode = 1):
'''
Abort the program
@param text:str Error message
@return returncode:int The programs return code
'''
printerr('\033[01;31m%s\033[00m' % text)
sys.exit(returncode)
class Parser:
''' '''
Bracket tree parser Bracket tree parser
''' '''
class Parser: @staticmethod
def parse(code):
''' '''
Parse a code and return a tree Parse a code and return a tree
@param code:str The code to parse @param code:str The code to parse
@return :list<|str> The root node in the tree @return :list<|str> The root node in the tree
''' '''
@staticmethod
def parse(code):
stack = [] stack = []
stackptr = -1 stackptr = -1
@ -74,6 +82,10 @@ class Parser:
quote = None quote = None
buf = None buf = None
col = 0
char = 0
line = 1
for charindex in range(0, len(code)): for charindex in range(0, len(code)):
c = code[charindex] c = code[charindex]
if comment: if comment:
@ -130,16 +142,26 @@ class Parser:
else: else:
buf += c buf += c
raise Exception('premature end of file') 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')
@staticmethod
def simplify(tree):
''' '''
Simplifies a tree Simplifies a tree
@param tree:list<|str> The tree @param tree:list<|str> The tree
''' '''
@staticmethod global variables
def simplify(tree):
program = tree[0] program = tree[0]
stack = [tree] stack = [tree]
while len(stack) > 0: while len(stack) > 0:
@ -159,6 +181,17 @@ class Parser:
new.append(alt[1]) new.append(alt[1])
break break
edited = True 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: else:
new.append(item) new.append(item)
else: else:
@ -171,10 +204,11 @@ class Parser:
class GeneratorBASH:
''' '''
Completion script generator for GNU Bash Completion script generator for GNU Bash
''' '''
class GeneratorBASH: def __init__(self, program, unargumented, argumented, variadic, suggestion, default):
''' '''
Constructor Constructor
@ -185,7 +219,6 @@ class GeneratorBASH:
@param suggestion:list<list<|str>> Specification of argument suggestions @param suggestion:list<list<|str>> Specification of argument suggestions
@param default:dict<str, list<str>>? Specification for optionless arguments @param default:dict<str, list<str>>? Specification for optionless arguments
''' '''
def __init__(self, program, unargumented, argumented, variadic, suggestion, default):
self.program = program self.program = program
self.unargumented = unargumented self.unargumented = unargumented
self.argumented = argumented self.argumented = argumented
@ -194,12 +227,12 @@ class GeneratorBASH:
self.default = default self.default = default
def __getSuggesters(self):
''' '''
Gets the argument suggesters for each option Gets the argument suggesters for each option
@return :dist<str, str> Map from option to suggester @return :dist<str, str> Map from option to suggester
''' '''
def __getSuggesters(self):
suggesters = {} suggesters = {}
for group in (self.unargumented, self.argumented, self.variadic): for group in (self.unargumented, self.argumented, self.variadic):
@ -221,12 +254,12 @@ class GeneratorBASH:
return suggesters return suggesters
def get(self):
''' '''
Returns the generated code Returns the generated code
@return :str The generated code @return :str The generated code
''' '''
def get(self):
buf = '# bash completion for %s -*- shell-script -*-\n\n' % self.program buf = '# bash completion for %s -*- shell-script -*-\n\n' % self.program
buf += '_%s()\n{\n' % self.program buf += '_%s()\n{\n' % self.program
buf += ' local cur prev words cword\n' buf += ' local cur prev words cword\n'
@ -248,7 +281,7 @@ class GeneratorBASH:
if functionType == 'pipe': if functionType == 'pipe':
return ' ( %s ) ' % (' | '.join(elems)) return ' ( %s ) ' % (' | '.join(elems))
if functionType == 'fullpipe': if functionType == 'fullpipe':
return ' ( %s ) ' % (' |% '.join(elems)) return ' ( %s ) ' % (' |& '.join(elems))
if functionType == 'cat': if functionType == 'cat':
return ' ( %s ) ' % (' ; '.join(elems)) return ' ( %s ) ' % (' ; '.join(elems))
if functionType == 'and': if functionType == 'and':
@ -330,11 +363,17 @@ class GeneratorBASH:
return buf return buf
@staticmethod
def where(command):
return '/share/bash-completion/completions/%s' % command
class GeneratorFISH:
''' '''
Completion script generator for fish Completion script generator for fish
''' '''
class GeneratorFISH: def __init__(self, program, unargumented, argumented, variadic, suggestion, default):
''' '''
Constructor Constructor
@ -345,7 +384,6 @@ class GeneratorFISH:
@param suggestion:list<list<|str>> Specification of argument suggestions @param suggestion:list<list<|str>> Specification of argument suggestions
@param default:dict<str, list<str>>? Specification for optionless arguments @param default:dict<str, list<str>>? Specification for optionless arguments
''' '''
def __init__(self, program, unargumented, argumented, variadic, suggestion, default):
self.program = program self.program = program
self.unargumented = unargumented self.unargumented = unargumented
self.argumented = argumented self.argumented = argumented
@ -354,12 +392,12 @@ class GeneratorFISH:
self.default = default self.default = default
def __getSuggesters(self):
''' '''
Gets the argument suggesters for each option Gets the argument suggesters for each option
@return :dist<str, str> Map from option to suggester @return :dist<str, str> Map from option to suggester
''' '''
def __getSuggesters(self):
suggesters = {} suggesters = {}
for group in (self.unargumented, self.argumented, self.variadic): for group in (self.unargumented, self.argumented, self.variadic):
@ -381,12 +419,12 @@ class GeneratorFISH:
return suggesters return suggesters
def __getFiles(self):
''' '''
Gets the file pattern for each option Gets the file pattern for each option
@return :dist<str, list<str>> Map from option to file pattern @return :dist<str, list<str>> Map from option to file pattern
''' '''
def __getFiles(self):
files = {} files = {}
for group in (self.unargumented, self.argumented, self.variadic): for group in (self.unargumented, self.argumented, self.variadic):
@ -408,12 +446,12 @@ class GeneratorFISH:
return files return files
def get(self):
''' '''
Returns the generated code Returns the generated code
@return :str The generated code @return :str The generated code
''' '''
def get(self):
buf = '# fish completion for %s -*- shell-script -*-\n\n' % self.program buf = '# fish completion for %s -*- shell-script -*-\n\n' % self.program
files = self.__getFiles() files = self.__getFiles()
@ -439,7 +477,7 @@ class GeneratorFISH:
if functionType == 'pipe': if functionType == 'pipe':
return ' ( %s ) ' % (' | '.join(elems)) return ' ( %s ) ' % (' | '.join(elems))
if functionType == 'fullpipe': if functionType == 'fullpipe':
return ' ( %s ) ' % (' |% '.join(elems)) return ' ( %s ) ' % (' |& '.join(elems))
if functionType == 'cat': if functionType == 'cat':
return ' ( %s ) ' % (' ; '.join(elems)) return ' ( %s ) ' % (' ; '.join(elems))
if functionType == 'and': if functionType == 'and':
@ -519,11 +557,17 @@ class GeneratorFISH:
return buf return buf
@staticmethod
def where(command):
return '/share/fish/completions/%s.fish' % command
class GeneratorZSH:
''' '''
Completion script generator for zsh Completion script generator for zsh
''' '''
class GeneratorZSH: def __init__(self, program, unargumented, argumented, variadic, suggestion, default):
''' '''
Constructor Constructor
@ -534,7 +578,6 @@ class GeneratorZSH:
@param suggestion:list<list<|str>> Specification of argument suggestions @param suggestion:list<list<|str>> Specification of argument suggestions
@param default:dict<str, list<str>>? Specification for optionless arguments @param default:dict<str, list<str>>? Specification for optionless arguments
''' '''
def __init__(self, program, unargumented, argumented, variadic, suggestion, default):
self.program = program self.program = program
self.unargumented = unargumented self.unargumented = unargumented
self.argumented = argumented self.argumented = argumented
@ -543,12 +586,12 @@ class GeneratorZSH:
self.default = default self.default = default
def __getSuggesters(self):
''' '''
Gets the argument suggesters for each option Gets the argument suggesters for each option
@return :dist<str, str> Map from option to suggester @return :dist<str, str> Map from option to suggester
''' '''
def __getSuggesters(self):
suggesters = {} suggesters = {}
for group in (self.unargumented, self.argumented, self.variadic): for group in (self.unargumented, self.argumented, self.variadic):
@ -570,12 +613,12 @@ class GeneratorZSH:
return suggesters return suggesters
def __getFiles(self):
''' '''
Gets the file pattern for each option Gets the file pattern for each option
@return :dist<str, list<str>> Map from option to file pattern @return :dist<str, list<str>> Map from option to file pattern
''' '''
def __getFiles(self):
files = {} files = {}
for group in (self.unargumented, self.argumented, self.variadic): for group in (self.unargumented, self.argumented, self.variadic):
@ -597,13 +640,13 @@ class GeneratorZSH:
return files return files
def get(self):
''' '''
Returns the generated code Returns the generated code
@return :str The generated code @return :str The generated code
''' '''
def get(self): buf = '#compdef %s\n\n' % self.program
buf = '# zsh completion for %s -*- shell-script -*-\n\n' % self.program
files = self.__getFiles() files = self.__getFiles()
@ -628,7 +671,7 @@ class GeneratorZSH:
if functionType == 'pipe': if functionType == 'pipe':
return ' ( %s ) ' % (' | '.join(elems)) return ' ( %s ) ' % (' | '.join(elems))
if functionType == 'fullpipe': if functionType == 'fullpipe':
return ' ( %s ) ' % (' |% '.join(elems)) return ' ( %s ) ' % (' |& '.join(elems))
if functionType == 'cat': if functionType == 'cat':
return ' ( %s ) ' % (' ; '.join(elems)) return ' ( %s ) ' % (' ; '.join(elems))
if functionType == 'and': if functionType == 'and':
@ -697,15 +740,20 @@ class GeneratorZSH:
return buf return buf
@staticmethod
def where(command):
return '/share/zsh/site-functions/_%s' % command
def main(shell, output, source):
''' '''
mane! mane!
@param shell:str Shell to generato completion for @param shell:str Shell for which to generate completion
@param output:str Output file @param output:str Output file
@param source:str Source file @param source:str Source file
''' '''
def main(shell, output, source):
with open(source, 'rb') as file: with open(source, 'rb') as file:
source = file.read().decode('utf8', 'replace') source = file.read().decode('utf8', 'replace')
source = Parser.parse(source) source = Parser.parse(source)
@ -730,20 +778,30 @@ def main(shell, output, source):
elif item[0] == 'default': elif item[0] == 'default':
default = item[1:]; default = item[1:];
for group in (unargumented, argumented, variadic): for (group, not_allowed) in ((unargumented, ['arg', 'suggest', 'files']), (argumented, []), (variadic, [])):
for index in range(0, len(group)): for index in range(0, len(group)):
item = group[index] item = group[index]
map = {} map = {}
for elem in item: 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:] map[elem[0]] = elem[1:]
group[index] = map group[index] = map
if default is not None: if default is not None:
map = {} map = {}
for elem in default: 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:] map[elem[0]] = elem[1:]
default = map default = map
generator = 'Generator' + shell.upper() generator = 'Generator' + shell.upper()
if generator not in globals():
abort('%s is not a supported shell' % shell)
generator = globals()[generator] generator = globals()[generator]
generator = generator(program, unargumented, argumented, variadic, suggestion, default) generator = generator(program, unargumented, argumented, variadic, suggestion, default)
code = generator.get() code = generator.get()
@ -753,48 +811,78 @@ def main(shell, output, source):
def where_main(shell, command):
''' '''
mane! --where mane!
'''
if __name__ == '__main__':
if len(sys.argv) != 6:
print("USAGE: auto-auto-complete SHELL --output OUTPUT_FILE --source SOURCE_FILE")
exit(1)
shell = sys.argv[1] @param shell:str Shell for which the completion should be installed
@param command:str The commmad name
'''
generator = 'Generator' + shell.upper()
if generator not in globals():
abort('%s is not a supported shell' % shell)
generator = globals()[generator]
print(generator.where(command))
# supermane!
if __name__ == '__main__':
if (len(sys.argv) == 1) or ((len(sys.argv) == 2) and (sys.argv[1] in ('-h', '--help'))):
print("USAGE: auto-auto-complete SHELL --output OUTPUT_FILE --source SOURCE_FILE [VARIABLE=VALUE...]")
print(" or: auto-auto-complete SHELL --where COMMAND")
exit(2)
shell = None
output = None output = None
source = None source = None
where = None
variables = {}
option = None option = None
aliases = {'-o' : '--output', aliases = {'-o' : '--output',
'-f' : '--source', '--file' : '--source', '-f' : '--source', '--file' : '--source',
'-s' : '--source'} '-s' : '--source',
'-w' : '--where'}
def useopt(option, arg): def useopt(option, arg):
global source global source
global output global output
global where
global variables
old = None old = None
if option == '--output': old = output; output = arg if option == '--output': old = output; output = arg
elif option == '--source': old = source; source = arg elif option == '--source': old = source; source = arg
elif option == '--where': old = where; where = arg
elif not option.startswith('-'):
if option not in variables:
variables[option] = []
variables[option].append(arg)
else: else:
raise Exception('Unrecognised option: ' + option) abort('Unrecognised option: ' + option)
if old is not None: if old is not None:
raise Exception('Duplicate option: ' + option) abort('Duplicate option: ' + option)
for arg in sys.argv[2:]: for arg in sys.argv[1:]:
if option is not None: if option is not None:
if option in aliases: if option in aliases:
option = aliases[option] option = aliases[option]
useopt(option, arg) useopt(option, arg)
option = None option = None
elif (shell is None) and not arg.startswith('-'):
shell = arg
else: else:
if '=' in arg: if '=' in arg:
useopt(arg[:index('=')], arg[index('=') + 1:]) useopt(arg[:arg.index('=')], arg[arg.index('=') + 1:])
else: else:
option = arg option = arg
if output is None: raise Exception('Unused option: --output') if shell is None:
if source is None: raise Exception('Unused option: --source') abort('No shell has been specified')
if where is None:
if output is None: abort('Unused option: --output')
if source is None: abort('Unused option: --source')
main(shell= shell, output= output, source= source) main(shell= shell, output= output, source= source)
else:
where_main(shell= shell, command= where)