From ae1e76535e6fb0fe9487161d78df993adff930b3 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Sat, 11 Oct 2014 19:11:59 +0200 Subject: prepare for adding info manual and auto-completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- .gitignore | 2 + Makefile | 221 ++++++++++-- auto-auto-complete.py | 841 ---------------------------------------------- doc/example | 64 ++++ doc/syntax | 78 +++++ example | 64 ---- src/auto-auto-complete.py | 841 ++++++++++++++++++++++++++++++++++++++++++++++ syntax | 78 ----- 8 files changed, 1175 insertions(+), 1014 deletions(-) delete mode 100755 auto-auto-complete.py create mode 100644 doc/example create mode 100644 doc/syntax delete mode 100644 example create mode 100755 src/auto-auto-complete.py delete mode 100644 syntax diff --git a/.gitignore b/.gitignore index 13ea8e3..699ce84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ _/ +bin/ +obj/ \#*\# .* !.git* diff --git a/Makefile b/Makefile index d974ec0..f4b1f9d 100644 --- a/Makefile +++ b/Makefile @@ -3,51 +3,210 @@ # notice and this notice are preserved. This file is offered as-is, # without any warranty. +# The package path prefix, if you want to install to another root, set DESTDIR to that root PREFIX = /usr -DATA = /share +# The command path excluding prefix BIN = /bin -PKGNAME = auto-auto-complete +# The resource path excluding prefix +DATA = /share +# The command path including prefix +BINDIR = $(PREFIX)$(BIN) +# The resource path including prefix +DATADIR = $(PREFIX)$(DATA) +# The generic documentation path including prefix +DOCDIR = $(DATADIR)/doc +# The info manual documentation path including prefix +INFODIR = $(DATADIR)/info +# The license base path including prefix +LICENSEDIR = $(DATADIR)/licenses + +# Python 3 command to use in shebangs SHEBANG = /usr$(BIN)/env python3 +# The name of the command as it should be installed COMMAND = auto-auto-complete -LICENSES = $(PREFIX)$(DATA) +# The name of the package as it should be installed +PKGNAME = auto-auto-complete + +# Build rules -all: auto-auto-complete #doc +.PHONY: default +default: command # info # shell -#doc: info -# -#info: auto-auto-complete.info.gz -# -#%.info.gz: info/%.texinfo -# makeinfo "$<" -# gzip -9 -f "$*.info" +.PHONY: all +all: command # doc # shell -auto-auto-complete: auto-auto-complete.py + +# Build rules for the command + +.PHONY: command +command: bin/auto-auto-complete + +bin/auto-auto-complete: src/auto-auto-complete.py + @mkdir -p bin cp "$<" "$@" sed -i 's:#!/usr/bin/env python3:#!$(SHEBANG):' "$@" -install: auto-auto-complete #auto-auto-complete.info.gz - install -dm755 "$(DESTDIR)$(PREFIX)$(BIN)" - install -m755 auto-auto-complete "$(DESTDIR)$(PREFIX)$(BIN)/$(COMMAND)" - install -dm755 "$(DESTDIR)$(LICENSES)/$(PKGNAME)" - install -m644 COPYING LICENSE "$(DESTDIR)$(LICENSES)/$(PKGNAME)" - install -dm755 "$(DESTDIR)$(PREFIX)$(DATA)/doc/$(PKGNAME)" - install -m644 example "$(DESTDIR)$(PREFIX)$(DATA)/doc/$(PKGNAME)" -# install -dm755 "$(DESTDIR)$(PREFIX)$(DATA)/info" -# install -m644 auto-auto-complete.info.gz "$(DESTDIR)$(PREFIX)$(DATA)/info/$(PKGNAME).info.gz" + +# Build rules for documentation + +.PHONY: doc +doc: info pdf dvi ps + +.PHONY: info +info: bin/auto-auto-complete.info +bin/%.info: info/%.texinfo info/fdl.texinfo + @mkdir -p bin + makeinfo $< + mv $*.pdf $@ + +.PHONY: pdf +pdf: bin/auto-auto-complete.pdf +bin/%.pdf: info/%.texinfo info/fdl.texinfo + @mkdir -p obj bin + cd obj ; yes X | texi2pdf ../$< + mv obj/$*.pdf $@ + +.PHONY: dvi +dvi: bin/auto-auto-complete.dvi +bin/%.dvi: info/%.texinfo info/fdl.texinfo + @mkdir -p obj bin + cd obj ; yes X | $(TEXI2DVI) ../$< + mv obj/$*.pdf $@ + +.PHONY: ps +ps: bin/auto-auto-complete.ps +bin/%.ps: info/%.texinfo info/fdl.texinfo + @mkdir -p obj bin + cd obj ; yes X | texi2pdf --ps ../$< + mv obj/$*.pdf $@ + + +# Build rules for shell auto-completion + +.PHONY: shell +shell: bash zsh fish + +.PHONY: bash +bash: bin/auto-auto-complete.bash +bin/auto-auto-complete.bash: src/completion bin/auto-auto-complete + @mkdir -p bin + bin/auto-auto-complete bash --output $@ --source $< + +.PHONY: zsh +zsh: bin/auto-auto-complete.zsh +bin/auto-auto-complete.zsh: src/completion bin/auto-auto-complete + @mkdir -p bin + bin/auto-auto-complete zsh --output $@ --source $< + +.PHONY: fish +fish: bin/auto-auto-complete.fish +bin/auto-auto-complete.fish: src/completion bin/auto-auto-complete + @mkdir -p bin + bin/auto-auto-complete fish --output $@ --source $< + + +# Install rules + +.PHONY: install +install: install-base install-examples # install-info # install-shell + +.PHONY: install +install-all: install-base install-doc # install-shell + +# Install base rules + +.PHONY: install-base +install-base: install-command install-license + +.PHONY: install-command-bin +install-command: bin/auto-auto-complete + install -dm755 -- "$(DESTDIR)$(BINDIR)" + install -m755 $< -- "$(DESTDIR)$(BINDIR)/$(COMMAND)" + +.PHONY: install-license +install-license: + install -dm755 -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)" + install -m644 COPYING LICENSE -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)" + +# Install documentation + +.PHONY: install-doc +install-doc: install-examples # install-info install-pdf install-ps install-dvi + +.PHONY: install-examples +install-examples: doc/example + install -dm755 -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME)" + install -m644 $^ -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME)/example" + +.PHONY: install-info +install-info: bin/auto-auto-complete.info + install -dm755 -- "$(DESTDIR)$(INFODIR)" + install -m644 $< -- "$(DESTDIR)$(INFODIR)/$(PKGNAME).info" + +.PHONY: install-pdf +install-pdf: bin/auto-auto-complete.pdf + install -dm755 -- "$(DESTDIR)$(DOCDIR)" + install -m644 $< -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME).pdf" + +.PHONY: install-ps +install-ps: bin/auto-auto-complete.ps + install -dm755 -- "$(DESTDIR)$(DOCDIR)" + install -m644 $< -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME).ps" + +.PHONY: install-dvi +install-dvi: bin/auto-auto-complete.dvi + install -dm755 -- "$(DESTDIR)$(DOCDIR)" + install -m644 $< -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME).dvi" + +# Install shell auto-completion + +.PHONY: install-shell +install-shell: install-bash install-zsh install-fish + +.PHONY: install-bash +install-bash: bin/auto-auto-complete.bash + install -dm755 -- "$(DESTDIR)$(DATADIR)/bash-completion/completions" + install -m644 $< -- "$(DESTDIR)$(DATADIR)/bash-completion/completions/$(COMMAND)" + +.PHONY: install-zsh +install-zsh: bin/auto-auto-complete.zsh + install -dm755 -- "$(DESTDIR)$(DATADIR)/zsh/site-functions" + install -m644 $< -- "$(DESTDIR)$(DATADIR)/zsh/site-functions/_$(COMMAND)" + +.PHONY: install-fish +install-fish: bin/auto-auto-complete.fish + install -dm755 -- "$(DESTDIR)$(DATADIR)/fish/completions" + install -m644 $< -- "$(DESTDIR)$(DATADIR)/fish/completions/$(COMMAND).fish" + + +# Uninstall rules uninstall: - rm -- "$(DESTDIR)$(PREFIX)$(BIN)/$(COMMAND)" - rm -- "$(DESTDIR)$(LICENSES)/$(PKGNAME)/COPYING" - rm -- "$(DESTDIR)$(LICENSES)/$(PKGNAME)/LICENSE" - rm -- "$(DESTDIR)$(PREFIX)$(DATA)/doc/$(PKGNAME)/example" - rmdir -- "$(DESTDIR)$(PREFIX)$(DATA)/doc/$(PKGNAME)" -# rmdir -- "$(DESTDIR)$(LICENSES)/$(PKGNAME)" -# rm -- "$(DESTDIR)$(PREFIX)$(DATA)/info/$(PKGNAME).info.gz" + -rm -- "$(DESTDIR)$(BINDIR)/$(COMMAND)" + -rm -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)/COPYING" + -rm -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)/LICENSE" + -rmdir -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)" + -rm -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME)/example" + -rmdir -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME)" + -rm -- "$(DESTDIR)$(INFODIR)/$(PKGNAME).info" + -rm -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME).pdf" + -rm -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME).ps" + -rm -- "$(DESTDIR)$(DOCDIR)/$(PKGNAME).dvi" + -rm -- "$(DESTDIR)$(DATADIR)/fish/completions/$(COMMAND).fish" + -rmdir -- "$(DESTDIR)$(DATADIR)/fish/completions" + -rmdir -- "$(DESTDIR)$(DATADIR)/fish" + -rm -- "$(DESTDIR)$(DATADIR)/zsh/site-functions/_$(COMMAND)" + -rmdir -- "$(DESTDIR)$(DATADIR)/zsh/site-functions" + -rmdir -- "$(DESTDIR)$(DATADIR)/zsh" + -rm -- "$(DESTDIR)$(DATADIR)/bash-completion/completions/$(COMMAND)" + -rmdir -- "$(DESTDIR)$(DATADIR)/bash-completion/completions" + -rmdir -- "$(DESTDIR)$(DATADIR)/bash-completion" -clean: - -rm -f auto-auto-complete #auto-auto-complete.info.gz +# Clean rules -.PHONY: all install uninstall clean +.PHONY: clean +clean: + -rm -fr bin obj diff --git a/auto-auto-complete.py b/auto-auto-complete.py deleted file mode 100755 index 35d8ab4..0000000 --- a/auto-auto-complete.py +++ /dev/null @@ -1,841 +0,0 @@ -#!/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 . -''' -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>> Specification of unargumented options - @param argumented:list>> Specification of argumented options - @param variadic:list>> Specification of variadic options - @param suggestion:list> Specification of argument suggestions - @param default:dict>? 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 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>> Specification of unargumented options - @param argumented:list>> Specification of argumented options - @param variadic:list>> Specification of variadic options - @param suggestion:list> Specification of argument suggestions - @param default:dict>? 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 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> 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>> Specification of unargumented options - @param argumented:list>> Specification of argumented options - @param variadic:list>> Specification of variadic options - @param suggestion:list> Specification of argument suggestions - @param default:dict>? 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 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> 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) - diff --git a/doc/example b/doc/example new file mode 100644 index 0000000..6212c06 --- /dev/null +++ b/doc/example @@ -0,0 +1,64 @@ +((value command ponysay) + (default (arg MESSAGE) (files -0) (suggest message) (desc 'Message spoken by the pony')) + + (multiple unargumented + ((options -h --help) (complete --help) (desc 'Show summary of options')) + ((options -v --version) (complete --version) (desc 'Show version of program')) + ((options -c --compress) (complete --compress) (desc 'Compress message')) + ((options -l --list) (complete --list) (desc 'List all MLP:FiM ponies')) + ((options -L --altlist --symlist) (complete --symlist) (desc 'List all MLP:FiM ponies, with symlink mapping')) + ((options +l ++list) (complete ++list) (desc 'List all non-MLP:FiM ponies')) + ((options +K ++altlist ++symlist) (complete ++symlist) (desc 'List all non-MLP:FiM ponies, with symlink mapping')) + ((options -A --all) (complete --all) (desc 'List all ponies')) + ((options +A ++all --altall --symall) (complete --symall) (desc 'List all ponies, with symlink mapping')) + ((options -b --bubblelist --balloonlist) (complete --balloonlist) (desc 'List all balloon styles')) + ((options -o --pony-only --ponyonly) (complete --pony-only) (desc 'Print just the pony')) + ((options -X --256-colours --256colours --x-colours) (desc 'Use xterm colours')) + ((options -V --tty-colours --ttycolours --vt-colours) (desc 'Use linux vt colours')) + ((options -K --kms-colours --kmscolours) (desc 'Utilise kms support')) + ) + + ; in files, -0 mean to not accept files, you can use to before other arguments to disallow them, + ; -a means accept all files, -d → directory, -D → door, -f → regular or pipe, -l → symlink (allowed by default on all), -r → regular + (multiple argumented + ((options -f --file --pony) (complete --file --pony) (arg PONY) (suggest pony-f) (files -f *.pony) (desc 'Specify the pony that should printed')) + ((options +f ++file ++pony) (complete ++file ++pony) (arg PONY) (suggest pony+f) (files -f *.pony) (desc 'Specify the extrapony that should printed')) + ((options -q --quote) (complete --quote) (arg PONY) (suggest pony-q) (files -f *.pony) (desc 'Specify the pony that should quote herself')) + ((options -b --bubble --balloon) (complete --balloon) (arg STYLE) (suggest balloon) (files -f (case (ponysay *.say) (ponyhink *.think))) (desc 'Specify message balloon style')) + ((options -W --wrap) (complete --wrap) (arg COLUMN) (suggest wrap) (files -0) (desc 'Specify wrapping column')) + ((options +c --colour) (complete --colour) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the balloon, balloon link and message')) + ((options --colour-bubble --colour-balloon) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the balloon')) + ((options --colour-link) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the balloon link')) + ((options --colour-msg --colour-message) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the message')) + ((options --colour-pony) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the pony (if uncoloured)')) + ((options --colour-wrap --colour-hyphen) (arg ANSI-COLOUR) (files -0) (desc 'Specify addition colour of wrapping hyphen')) + ) + + ; `bind` copies everything that is missing except `options` and `suggest` + (variadic (options --f --files --ponies) (bind -f) (desc 'Specify the ponies that may be printed')) + (variadic (options ++f ++files ++ponies) (bind +f) (desc 'Specify the extraponies that may be printed')) + (variadic (options --q --quotes) (bind -q) (desc 'Specify the pony that may quote themself')) + + (suggestion message (verbatim MESSAGE) + ) + (suggestion pony-f (exec "'/usr/bin/ponysay'" --onelist) + (no-exec ls "'/usr/share/ponysay/ponies'" .pony) + ) + (suggestion pony+f (exec "'/usr/bin/ponysay'" ++onelist) + (no-exec ls "'/usr/share/ponysay/extraponies'" .pony) + ) + (suggestion pony-q (exec "'/usr/bin/ponysay'" --quoters) + (no-exec ls "'/usr/share/ponysay/ponies'" .pony) + ) + (suggestion balloon (exec "'/usr/bin/ponysay'" --balloonlist) + (no-exec ls "'/usr/share/ponysay/balloons'" (case (ponysay .say) (ponythink .think))) + ) + (suggestion wrap (verbatim none inherit 100 60) + (calc (pipe (stty size) + (cut -d ' ' -f 2) + ) - 10 + ) + ) + ; in addition to `pipe`(|) to following are also possible `fullpipe`(|&) `cat`(;) `and`(&&) `or`(||) +) + diff --git a/doc/syntax b/doc/syntax new file mode 100644 index 0000000..ca44109 --- /dev/null +++ b/doc/syntax @@ -0,0 +1,78 @@ +auto-auto-complete ::= _ program _ + +program ::= '(' _ value _ body ')' + +body ::= [{(multiple | default | unargumented | argumented | variadic | suggestion) _}] + +_value ::= __ name | _ value | _ case + +value ::= name | variable | case + +variable ::= '(' _ "value" __ name _ [{value _}] ')' + +case ::= '(' _ "case" {_ a_case} _ ')' + +a_case ::= '(' {_ value} _ ')' + +multiple ::= '(' _ "multiple" [{_ '(' _ _ ')'}] _ ')' + ==> [{'(' ' ' ')'}] + +anything ::= [{_ value | _ '(' anything _ ')'}] + +default ::= '(' _ "default" (4-:_ ) _ ')' + +unargumented ::= '(' _ "unargumented" (3-:_ (options | complete | desc)) _ ')' + +argumented ::= '(' _ "argumented" (6-:_ (options | complete | arg | suggest | files | bind | desc)) _ ')' + +variadic ::= '(' _ "variadic" (6-:_ (options | complete | arg | suggest | files | bind | desc)) _ ')' + +options ::= '(' _ "options" _value [{_ value}] ')' + +complete ::= '(' _ "complete" _value [{_ value}] ')' + +arg ::= '(' _ "arg" _value ')' + +suggest ::= '(' _ "suggest" _value ')' + +files ::= '(' _ "files" _value [{_ value}] ')' +::= {"-0" | "-a" | "-d" | "-D" | "-f" | "-l" | "-r"} [{value}] + +bind ::= '(' _ "bind" _value ')' + +desc ::= '(' _ "desc" _value ')' + +suggestion ::= '(' _ "suggestion" _ value {_ a_suggestion} _ ')' + +a_suggestion ::= '(' _ (exec | noexec | ls | verbatim | calc) _ ')' + +exec ::= 'exec' (__ value | _ any_exec) [{_ value | _ any_exec}] + +noexec ::= ls | verbatim + +ls ::= 'ls' _value [_ value] + +verbatim ::= 'verbatim' _value [{_ value}] + +calc ::= 'calc' (__ value | _ any_exec) [{_ value | _ any_exec}] + +any_exec ::= '(' _ exec_type (__ value | _ any_exec) [{_ value | _ any_exec}] _ ')' + +exec_type ::= "exec" | "calc" | "pipe" | "fullpipe" | "cat" | "and" | "or" + +name ::= {letter | escape | squote | dquote} + +letter :: $any ^ ' ' ^ \t ^ \n ^ \r ^ \t ^ '\' ^ \' ^ \" + +escape ::= '\' $any + +squote ::= \' [{$any ^ '\' ^ \' | escape}] \' + +dquote ::= \" [{$any ^ '\' ^ \" | escape}] \" + +comment ::= ('#' | ';') [{$any}] \z + +__ ::= {' ' | \t | \n | \r | \f | comment} + +_ ::= [__] + diff --git a/example b/example deleted file mode 100644 index 6212c06..0000000 --- a/example +++ /dev/null @@ -1,64 +0,0 @@ -((value command ponysay) - (default (arg MESSAGE) (files -0) (suggest message) (desc 'Message spoken by the pony')) - - (multiple unargumented - ((options -h --help) (complete --help) (desc 'Show summary of options')) - ((options -v --version) (complete --version) (desc 'Show version of program')) - ((options -c --compress) (complete --compress) (desc 'Compress message')) - ((options -l --list) (complete --list) (desc 'List all MLP:FiM ponies')) - ((options -L --altlist --symlist) (complete --symlist) (desc 'List all MLP:FiM ponies, with symlink mapping')) - ((options +l ++list) (complete ++list) (desc 'List all non-MLP:FiM ponies')) - ((options +K ++altlist ++symlist) (complete ++symlist) (desc 'List all non-MLP:FiM ponies, with symlink mapping')) - ((options -A --all) (complete --all) (desc 'List all ponies')) - ((options +A ++all --altall --symall) (complete --symall) (desc 'List all ponies, with symlink mapping')) - ((options -b --bubblelist --balloonlist) (complete --balloonlist) (desc 'List all balloon styles')) - ((options -o --pony-only --ponyonly) (complete --pony-only) (desc 'Print just the pony')) - ((options -X --256-colours --256colours --x-colours) (desc 'Use xterm colours')) - ((options -V --tty-colours --ttycolours --vt-colours) (desc 'Use linux vt colours')) - ((options -K --kms-colours --kmscolours) (desc 'Utilise kms support')) - ) - - ; in files, -0 mean to not accept files, you can use to before other arguments to disallow them, - ; -a means accept all files, -d → directory, -D → door, -f → regular or pipe, -l → symlink (allowed by default on all), -r → regular - (multiple argumented - ((options -f --file --pony) (complete --file --pony) (arg PONY) (suggest pony-f) (files -f *.pony) (desc 'Specify the pony that should printed')) - ((options +f ++file ++pony) (complete ++file ++pony) (arg PONY) (suggest pony+f) (files -f *.pony) (desc 'Specify the extrapony that should printed')) - ((options -q --quote) (complete --quote) (arg PONY) (suggest pony-q) (files -f *.pony) (desc 'Specify the pony that should quote herself')) - ((options -b --bubble --balloon) (complete --balloon) (arg STYLE) (suggest balloon) (files -f (case (ponysay *.say) (ponyhink *.think))) (desc 'Specify message balloon style')) - ((options -W --wrap) (complete --wrap) (arg COLUMN) (suggest wrap) (files -0) (desc 'Specify wrapping column')) - ((options +c --colour) (complete --colour) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the balloon, balloon link and message')) - ((options --colour-bubble --colour-balloon) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the balloon')) - ((options --colour-link) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the balloon link')) - ((options --colour-msg --colour-message) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the message')) - ((options --colour-pony) (arg ANSI-COLOUR) (files -0) (desc 'Specify colour of the pony (if uncoloured)')) - ((options --colour-wrap --colour-hyphen) (arg ANSI-COLOUR) (files -0) (desc 'Specify addition colour of wrapping hyphen')) - ) - - ; `bind` copies everything that is missing except `options` and `suggest` - (variadic (options --f --files --ponies) (bind -f) (desc 'Specify the ponies that may be printed')) - (variadic (options ++f ++files ++ponies) (bind +f) (desc 'Specify the extraponies that may be printed')) - (variadic (options --q --quotes) (bind -q) (desc 'Specify the pony that may quote themself')) - - (suggestion message (verbatim MESSAGE) - ) - (suggestion pony-f (exec "'/usr/bin/ponysay'" --onelist) - (no-exec ls "'/usr/share/ponysay/ponies'" .pony) - ) - (suggestion pony+f (exec "'/usr/bin/ponysay'" ++onelist) - (no-exec ls "'/usr/share/ponysay/extraponies'" .pony) - ) - (suggestion pony-q (exec "'/usr/bin/ponysay'" --quoters) - (no-exec ls "'/usr/share/ponysay/ponies'" .pony) - ) - (suggestion balloon (exec "'/usr/bin/ponysay'" --balloonlist) - (no-exec ls "'/usr/share/ponysay/balloons'" (case (ponysay .say) (ponythink .think))) - ) - (suggestion wrap (verbatim none inherit 100 60) - (calc (pipe (stty size) - (cut -d ' ' -f 2) - ) - 10 - ) - ) - ; in addition to `pipe`(|) to following are also possible `fullpipe`(|&) `cat`(;) `and`(&&) `or`(||) -) - 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 . +''' +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>> Specification of unargumented options + @param argumented:list>> Specification of argumented options + @param variadic:list>> Specification of variadic options + @param suggestion:list> Specification of argument suggestions + @param default:dict>? 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 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>> Specification of unargumented options + @param argumented:list>> Specification of argumented options + @param variadic:list>> Specification of variadic options + @param suggestion:list> Specification of argument suggestions + @param default:dict>? 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 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> 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>> Specification of unargumented options + @param argumented:list>> Specification of argumented options + @param variadic:list>> Specification of variadic options + @param suggestion:list> Specification of argument suggestions + @param default:dict>? 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 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> 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) + diff --git a/syntax b/syntax deleted file mode 100644 index ca44109..0000000 --- a/syntax +++ /dev/null @@ -1,78 +0,0 @@ -auto-auto-complete ::= _ program _ - -program ::= '(' _ value _ body ')' - -body ::= [{(multiple | default | unargumented | argumented | variadic | suggestion) _}] - -_value ::= __ name | _ value | _ case - -value ::= name | variable | case - -variable ::= '(' _ "value" __ name _ [{value _}] ')' - -case ::= '(' _ "case" {_ a_case} _ ')' - -a_case ::= '(' {_ value} _ ')' - -multiple ::= '(' _ "multiple" [{_ '(' _ _ ')'}] _ ')' - ==> [{'(' ' ' ')'}] - -anything ::= [{_ value | _ '(' anything _ ')'}] - -default ::= '(' _ "default" (4-:_ ) _ ')' - -unargumented ::= '(' _ "unargumented" (3-:_ (options | complete | desc)) _ ')' - -argumented ::= '(' _ "argumented" (6-:_ (options | complete | arg | suggest | files | bind | desc)) _ ')' - -variadic ::= '(' _ "variadic" (6-:_ (options | complete | arg | suggest | files | bind | desc)) _ ')' - -options ::= '(' _ "options" _value [{_ value}] ')' - -complete ::= '(' _ "complete" _value [{_ value}] ')' - -arg ::= '(' _ "arg" _value ')' - -suggest ::= '(' _ "suggest" _value ')' - -files ::= '(' _ "files" _value [{_ value}] ')' -::= {"-0" | "-a" | "-d" | "-D" | "-f" | "-l" | "-r"} [{value}] - -bind ::= '(' _ "bind" _value ')' - -desc ::= '(' _ "desc" _value ')' - -suggestion ::= '(' _ "suggestion" _ value {_ a_suggestion} _ ')' - -a_suggestion ::= '(' _ (exec | noexec | ls | verbatim | calc) _ ')' - -exec ::= 'exec' (__ value | _ any_exec) [{_ value | _ any_exec}] - -noexec ::= ls | verbatim - -ls ::= 'ls' _value [_ value] - -verbatim ::= 'verbatim' _value [{_ value}] - -calc ::= 'calc' (__ value | _ any_exec) [{_ value | _ any_exec}] - -any_exec ::= '(' _ exec_type (__ value | _ any_exec) [{_ value | _ any_exec}] _ ')' - -exec_type ::= "exec" | "calc" | "pipe" | "fullpipe" | "cat" | "and" | "or" - -name ::= {letter | escape | squote | dquote} - -letter :: $any ^ ' ' ^ \t ^ \n ^ \r ^ \t ^ '\' ^ \' ^ \" - -escape ::= '\' $any - -squote ::= \' [{$any ^ '\' ^ \' | escape}] \' - -dquote ::= \" [{$any ^ '\' ^ \" | escape}] \" - -comment ::= ('#' | ';') [{$any}] \z - -__ ::= {' ' | \t | \n | \r | \f | comment} - -_ ::= [__] - -- cgit v1.2.3-70-g09d2