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,40 +29,51 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys 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'): def print(text = '', end = '\n'):
'''
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)
'''
sys.stdout.buffer.write((str(text) + end).encode('utf-8')) 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'): def printerr(text = '', end = '\n'):
'''
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)
'''
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)
'''
Bracket tree parser
'''
class Parser: class Parser:
'''
Bracket tree 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:
'''
Completion script generator for GNU Bash
'''
class GeneratorBASH: class GeneratorBASH:
'''
Completion script generator for GNU Bash
'''
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
'''
Completion script generator for fish
'''
class GeneratorFISH: class GeneratorFISH:
'''
Completion script generator for fish
'''
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
'''
Completion script generator for zsh
'''
class GeneratorZSH: class GeneratorZSH:
'''
Completion script generator for zsh
'''
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
'''
mane!
@param shell:str Shell to generato completion for
@param output:str Output file
@param source:str Source file
'''
def main(shell, output, source): def main(shell, output, source):
'''
mane!
@param shell:str Shell for which to generate completion
@param output:str Output file
@param source:str Source file
'''
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)