Work on improving the output of the manpage completion script

This commit is contained in:
ridiculousfish 2012-04-03 18:38:25 -07:00
parent 37defa739b
commit 7394374a15

View file

@ -14,9 +14,9 @@ Redistributions in binary form must reproduce the above copyright notice, this l
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" """
""" Run me like this: ./create_manpage_completions.py /usr/share/man/man1/* > test2.out """ """ Run me like this: ./create_manpage_completions.py /usr/share/man/man1/* > man_completions.fish """
import sys, re, os.path, gzip, traceback import string, sys, re, os.path, gzip, traceback
# This gets set to the name of the command that we are currently executing # This gets set to the name of the command that we are currently executing
CMDNAME = "" CMDNAME = ""
@ -24,6 +24,24 @@ CMDNAME = ""
# builtcommand writes into this global variable, yuck # builtcommand writes into this global variable, yuck
built_command_output = [] built_command_output = []
# Diagnostic output
diagnostic_output = []
diagnostic_indent = 0
def add_diagnostic(dgn):
diagnostic_output.append(' '*diagnostic_indent + dgn)
def flush_diagnostics(where):
if diagnostic_output:
output_str = '\n'.join(diagnostic_output) + '\n'
where.write(output_str)
diagnostic_output[:] = []
# Make sure we don't output the same completion multiple times, which can happen
# For example, xsubpp.1.gz and xsubpp5.10.1.gz
# This maps commands to lists of completions
already_output_completions = {}
def compileAndSearch(regex, input): def compileAndSearch(regex, input):
options_section_regex = re.compile(regex , re.DOTALL) options_section_regex = re.compile(regex , re.DOTALL)
options_section_matched = re.search( options_section_regex, input) options_section_matched = re.search( options_section_regex, input)
@ -43,34 +61,64 @@ def unquoteSingleQuotes(data):
data = data[1:len(data)-1] data = data[1:len(data)-1]
return data return data
# Make a string of characters that are deemed safe in fish without needing to be escaped
# Note that space is not included
g_fish_safe_chars = frozenset(string.ascii_letters + string.digits + '_+-|/:=<>@~')
def fish_escape_single_quote(str):
# Escape a string if necessary so that it can be put in single quotes
# If it has no non-safe chars, there's nothing to do
if g_fish_safe_chars.issuperset(str):
return str
str = str.replace('\\', '\\\\') # Replace one backslash with two
str = str.replace("'", "\\'") # Replace one single quote with a backslash-single-quote
return "'" + str + "'"
def output_complete_command(cmdname, args, description, output_list): def output_complete_command(cmdname, args, description, output_list):
comps = ['complete -c', cmdname] comps = ['complete -c', cmdname]
comps.extend(args) comps.extend(args)
if description:
comps.append('--description') comps.append('--description')
comps.append("'" + description + "'") comps.append(description)
output_list.append(' '.join(comps)) output_list.append(' '.join(comps))
def builtcommand(options, description): def builtcommand(options, description):
# print "Options are: ", options # print "Options are: ", options
optionlist = re.split(" |,|\"|=|[|]", options) man_optionlist = re.split(" |,|\"|=|[|]", options)
optionlist = [x for x in optionlist if x.startswith('-')] fish_options = []
if len(optionlist) == 0: for option in man_optionlist:
return if option.startswith('--'):
for i in range(0, len(optionlist)): # New style long option (--recursive)
if optionlist[i][0:2] == "--": fish_options.append('-l ' + fish_escape_single_quote(option[2:]))
optionlist[i] = "-l " + optionlist[i][2:] elif option.startswith('-') and len(option) == 2:
else: # New style short option (-r)
optionlist[i] = "-s " + optionlist[i][1:] fish_options.append('-s ' + fish_escape_single_quote(option[1:]))
description = description.replace('"','\\"') elif option.startswith('-') and len(option) > 2:
# Old style long option (-recursive)
fish_options.append('-o ' + fish_escape_single_quote(option[1:]))
# Determine which options are new (not already in existing_options)
# Then add those to the existing options
existing_options = already_output_completions.setdefault(CMDNAME, set())
fish_options = [opt for opt in fish_options if opt not in existing_options]
existing_options.update(fish_options)
# Maybe it's all for naught
if not fish_options: return
first_period = description.find(".") first_period = description.find(".")
# print "First period at: ", first_period if first_period >= 45 or (first_period == -1 and len(description) > 45):
if first_period >= 45 or first_period == -1 and len(description) > 45: description = description[:45] + '... [See Man Page]'
description = description[:45] + "... [See Man Page]"
elif first_period >= 0: elif first_period >= 0:
description = description[:first_period] description = description[:first_period]
output_complete_command(CMDNAME, optionlist, description, built_command_output) # Escape some more things
description = fish_escape_single_quote(description)
escaped_cmd = fish_escape_single_quote(CMDNAME)
output_complete_command(escaped_cmd, fish_options, description, built_command_output)
@ -129,10 +177,10 @@ class Type1ManParser(ManParser):
options_parts_regex = re.compile("\.PP(.*?)\.RE", re.DOTALL) options_parts_regex = re.compile("\.PP(.*?)\.RE", re.DOTALL)
options_matched = re.search(options_parts_regex, options_section) options_matched = re.search(options_parts_regex, options_section)
# print options_matched # print options_matched
print >> sys.stderr, "Command is ", CMDNAME add_diagnostic('Command is ' + CMDNAME)
if options_matched == None: if options_matched == None:
print >> sys.stderr, "Unable to find options" add_diagnostic('Unable to find options')
if( self.fallback(options_section) ): if( self.fallback(options_section) ):
return True return True
elif (self.fallback2(options_section) ): elif (self.fallback2(options_section) ):
@ -154,7 +202,7 @@ class Type1ManParser(ManParser):
optionName = data[0].strip() optionName = data[0].strip()
if ( optionName.find("-") == -1): if ( optionName.find("-") == -1):
print >> sys.stderr, optionName, " doesn't contain - " add_diagnostic(optionName + " doesn't contain - ")
# return False # return False
else: else:
optionName = unquoteDoubleQuotes(optionName) optionName = unquoteDoubleQuotes(optionName)
@ -164,18 +212,18 @@ class Type1ManParser(ManParser):
builtcommand(optionName, optionDescription) builtcommand(optionName, optionDescription)
else: else:
print >> sys.stderr, "Unable to split option from description" add_diagnostic('Unable to split option from description')
return False return False
options_section = options_section[options_matched.end()-3:] options_section = options_section[options_matched.end()-3:]
options_matched = re.search(options_parts_regex, options_section) options_matched = re.search(options_parts_regex, options_section)
def fallback(self, options_section): def fallback(self, options_section):
print >> sys.stderr, "Falling Back" add_diagnostic('Falling Back')
options_parts_regex = re.compile("\.TP( \d+)?(.*?)\.TP", re.DOTALL) options_parts_regex = re.compile("\.TP( \d+)?(.*?)\.TP", re.DOTALL)
options_matched = re.search(options_parts_regex, options_section) options_matched = re.search(options_parts_regex, options_section)
if options_matched == None: if options_matched == None:
print >> sys.stderr, "Still not found" add_diagnostic('Still not found')
return False return False
while options_matched != None: while options_matched != None:
data = options_matched.group(2) data = options_matched.group(2)
@ -185,7 +233,7 @@ class Type1ManParser(ManParser):
if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400): if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400):
optionName = data[0].strip() optionName = data[0].strip()
if ( optionName.find("-") == -1): if ( optionName.find("-") == -1):
print >> sys.stderr, optionName, "doesn't contains -" add_diagnostic(optionName + "doesn't contains -")
else: else:
optionName = unquoteDoubleQuotes(optionName) optionName = unquoteDoubleQuotes(optionName)
optionName = unquoteSingleQuotes(optionName) optionName = unquoteSingleQuotes(optionName)
@ -193,8 +241,7 @@ class Type1ManParser(ManParser):
# print "Option: ", optionName," Description: ", optionDescription , '\n' # print "Option: ", optionName," Description: ", optionDescription , '\n'
builtcommand(optionName, optionDescription) builtcommand(optionName, optionDescription)
else: else:
print >> sys.stderr, data add_diagnostic('Unable to split option from description')
print >> sys.stderr, "Unable to split option from description"
return False return False
options_section = options_section[options_matched.end()-3:] options_section = options_section[options_matched.end()-3:]
@ -202,7 +249,7 @@ class Type1ManParser(ManParser):
return True return True
def fallback2(self, options_section): def fallback2(self, options_section):
print >> sys.stderr, "Falling Back2" add_diagnostic('Falling Back2')
ix_remover_regex = re.compile("\.IX.*") ix_remover_regex = re.compile("\.IX.*")
trailing_num_regex = re.compile('\\d+$') trailing_num_regex = re.compile('\\d+$')
options_parts_regex = re.compile("\.IP (.*?)\.IP", re.DOTALL) options_parts_regex = re.compile("\.IP (.*?)\.IP", re.DOTALL)
@ -210,7 +257,7 @@ class Type1ManParser(ManParser):
options_section = re.sub(ix_remover_regex, "", options_section) options_section = re.sub(ix_remover_regex, "", options_section)
options_matched = re.search(options_parts_regex, options_section) options_matched = re.search(options_parts_regex, options_section)
if options_matched == None: if options_matched == None:
print >> sys.stderr, "Still not found2" add_diagnostic('Still not found2')
return False return False
while options_matched != None: while options_matched != None:
data = options_matched.group(1) data = options_matched.group(1)
@ -225,8 +272,8 @@ class Type1ManParser(ManParser):
# data = re.sub(trailing_num_regex, "", data) # data = re.sub(trailing_num_regex, "", data)
optionName = re.sub(trailing_num_regex, "", data[0].strip()) optionName = re.sub(trailing_num_regex, "", data[0].strip())
if ( optionName.find("-") == -1): if ('-' not in optionName):
print >> sys.stderr, optionName, "doesn't contains -" add_diagnostic(optionName + " doesn't contain -")
else: else:
optionName = optionName.strip() optionName = optionName.strip()
optionName = unquoteDoubleQuotes(optionName) optionName = unquoteDoubleQuotes(optionName)
@ -236,7 +283,7 @@ class Type1ManParser(ManParser):
builtcommand(optionName, optionDescription) builtcommand(optionName, optionDescription)
else: else:
# print data # print data
print >> sys.stderr, "Unable to split option from description" add_diagnostic('Unable to split option from description')
return False return False
options_section = options_section[options_matched.end()-3:] options_section = options_section[options_matched.end()-3:]
@ -274,10 +321,10 @@ class Type2ManParser(ManParser):
options_parts_regex = re.compile("\.[I|T]P( \d+(\.\d)?i?)?(.*?)\.[I|T]P", re.DOTALL) options_parts_regex = re.compile("\.[I|T]P( \d+(\.\d)?i?)?(.*?)\.[I|T]P", re.DOTALL)
# options_parts_regex = re.compile("\.TP(.*?)[(\.TP)|(\.SH)]", re.DOTALL) # options_parts_regex = re.compile("\.TP(.*?)[(\.TP)|(\.SH)]", re.DOTALL)
options_matched = re.search(options_parts_regex, options_section) options_matched = re.search(options_parts_regex, options_section)
print >> sys.stderr, "Command is ", CMDNAME add_diagnostic('Command is ' + CMDNAME)
if options_matched == None: if options_matched == None:
print >> sys.stderr, self.name() + ": Unable to find options" add_diagnostic(self.name() + ': Unable to find options')
return False return False
while (options_matched != None): while (options_matched != None):
@ -292,8 +339,8 @@ class Type2ManParser(ManParser):
# print >> sys.stderr, data # print >> sys.stderr, data
if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400): if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400):
optionName = data[0].strip() optionName = data[0].strip()
if ( optionName.find("-") == -1): if '-' not in optionName:
print >> sys.stderr, optionName, "doesn't contains -" add_diagnostic(optionName + " doesn't contain -")
else: else:
optionName = unquoteDoubleQuotes(optionName) optionName = unquoteDoubleQuotes(optionName)
optionName = unquoteSingleQuotes(optionName) optionName = unquoteSingleQuotes(optionName)
@ -301,8 +348,8 @@ class Type2ManParser(ManParser):
# print "Option: ", optionName," Description: ", optionDescription , '\n' # print "Option: ", optionName," Description: ", optionDescription , '\n'
builtcommand(optionName, optionDescription) builtcommand(optionName, optionDescription)
else: else:
print >> sys.stderr, data # print >> sys.stderr, data
print >> sys.stderr, "Unable to split option from description" add_diagnostic('Unable to split option from description')
# return False # return False
@ -333,10 +380,10 @@ class Type3ManParser(ManParser):
# sys.exit(1) # sys.exit(1)
options_parts_regex = re.compile("\.TP(.*?)\.TP", re.DOTALL) options_parts_regex = re.compile("\.TP(.*?)\.TP", re.DOTALL)
options_matched = re.search(options_parts_regex, options_section) options_matched = re.search(options_parts_regex, options_section)
print >> sys.stderr, "Command is ", CMDNAME add_diagnostic('Command is ' + CMDNAME)
if options_matched == None: if options_matched == None:
print >> sys.stderr, "Unable to find options section" add_diagnostic('Unable to find options section')
return False return False
while (options_matched != None): while (options_matched != None):
@ -350,7 +397,7 @@ class Type3ManParser(ManParser):
if (len(data)>1): # and len(data[1])<400): if (len(data)>1): # and len(data[1])<400):
optionName = data[0].strip() optionName = data[0].strip()
if ( optionName.find("-") == -1): if ( optionName.find("-") == -1):
print >> sys.stderr, optionName, "doesn't contains -" add_diagnostic(optionName + "doesn't contain -")
else: else:
optionName = unquoteDoubleQuotes(optionName) optionName = unquoteDoubleQuotes(optionName)
optionName = unquoteSingleQuotes(optionName) optionName = unquoteSingleQuotes(optionName)
@ -359,7 +406,7 @@ class Type3ManParser(ManParser):
builtcommand(optionName, optionDescription) builtcommand(optionName, optionDescription)
else: else:
print >> sys.stderr, "Unable to split option from description" add_diagnostic('Unable to split option from description')
return False return False
options_section = options_section[options_matched.end()-3:] options_section = options_section[options_matched.end()-3:]
@ -415,7 +462,7 @@ class Type4ManParser(ManParser):
builtcommand(optionName, optionDescription) builtcommand(optionName, optionDescription)
else: else:
print >> sys.stderr, "Unable to split option from description" add_diagnostic('Unable to split option from description')
return False return False
options_section = options_section[options_matched.end()-3:] options_section = options_section[options_matched.end()-3:]
@ -485,11 +532,19 @@ class TypeMacManParser(ManParser):
def parse_manpage_at_path(manpage_path): def parse_manpage_at_path(manpage_path):
filename = os.path.basename(manpage_path) filename = os.path.basename(manpage_path)
# Clear diagnostics
global diagnostic_indent
diagnostic_output[:] = []
diagnostic_indent = 0
# Get the "base" command, e.g. gcc.1.gz -> gcc # Get the "base" command, e.g. gcc.1.gz -> gcc
global CMDNAME global CMDNAME
CMDNAME = filename.split('.', 1)[0] CMDNAME = filename.split('.', 1)[0]
print >> sys.stderr, "Considering " + manpage_path # Set up some diagnostics
add_diagnostic('Considering ' + manpage_path)
diagnostic_indent += 1
if manpage_path.endswith('.gz'): if manpage_path.endswith('.gz'):
fd = gzip.open(manpage_path, 'r') fd = gzip.open(manpage_path, 'r')
else: else:
@ -497,55 +552,57 @@ def parse_manpage_at_path(manpage_path):
manpage = fd.read() manpage = fd.read()
fd.close() fd.close()
parsers = [Type1ManParser(), Type2ManParser(), Type4ManParser(), Type3ManParser(), TypeMacManParser()]
parserToUse = None
# Get the "base" command, e.g. gcc.1.gz -> gcc # Get the "base" command, e.g. gcc.1.gz -> gcc
cmd_base = CMDNAME.split('.', 1)[0] cmd_base = CMDNAME.split('.', 1)[0]
ignoredcommands = ["cc", "g++", "gcc", "c++", "cpp", "emacs", "gprof", "wget", "ld", "awk"] ignoredcommands = ["cc", "g++", "gcc", "c++", "cpp", "emacs", "gprof", "wget", "ld", "awk"]
if cmd_base in ignoredcommands: if cmd_base in ignoredcommands:
return return
idx = 0
for parser in parsers:
if parser.isMyType(manpage):
parserToUse = parser
# print "Type is: ", parser.name()
break
idx += 1
# Clear the output list # Clear the output list
built_command_output[:] = [] built_command_output[:] = []
if parserToUse == None: parsers = [Type1ManParser(), Type2ManParser(), Type4ManParser(), Type3ManParser(), TypeMacManParser()]
print >> sys.stderr, manpage_path, " : Not supported" parsersToTry = [p for p in parsers if p.isMyType(manpage)]
if not parsersToTry:
add_diagnostic(manpage_path + ": Not supported")
else: else:
if parserToUse.parseManPage(manpage) == False: success = False
print >> sys.stderr, "Type%d : %s is unparsable" % (idx, manpage_path) for parser in parsersToTry:
elif built_command_output: add_diagnostic('Trying parser ' + parser.name())
diagnostic_indent += 1
success = parser.parseManPage(manpage)
diagnostic_indent -= 1
if success: break
if success:
built_command_output.insert(0, "# %s: %s" % (CMDNAME, parser.name())) built_command_output.insert(0, "# %s: %s" % (CMDNAME, parser.name()))
for line in built_command_output: for line in built_command_output:
pass print line
#print line print ''
#print '' add_diagnostic(manpage_path + ' parsed successfully')
print >> sys.stderr, manpage_path, " parsed successfully" else:
parser_names = ', '.join(p.name() for p in parsersToTry)
add_diagnostic('%s is unparsable (tried parser %s)' % (manpage_path, parser_names))
def compare_paths(a, b): def compare_paths(a, b):
""" Compare two paths by their base name, case insensitive """ """ Compare two paths by their base name, case insensitive """
return cmp(os.path.basename(a).lower(), os.path.basename(b).lower()) return cmp(os.path.basename(a).lower(), os.path.basename(b).lower())
def parse_and_output_man_pages(paths): def parse_and_output_man_pages(paths):
global diagnostic_indent
paths.sort(compare_paths) paths.sort(compare_paths)
for manpage_path in paths: for manpage_path in paths:
try: try:
parse_manpage_at_path(manpage_path) parse_manpage_at_path(manpage_path)
except IOError: except IOError:
print >> sys.stderr, 'Cannot open ', manpage_path diagnostic_indent = 0
add_diagnostic('Cannot open ' + manpage_path)
except: except:
print >> sys.stderr, "Error parsing %s: %s" % (manpage_path, sys.exc_info()[0]) add_diagnostic('Error parsing %s: %s' % (manpage_path, sys.exc_info()[0]))
traceback.print_exc(file=sys.stdout) flush_diagnostics(sys.stderr)
traceback.print_exc(file=sys.stderr)
flush_diagnostics(sys.stderr)