Don't generate completions if we already have bespoke completions in the data directory

Fixes https://github.com/fish-shell/fish-shell/issues/148
Also fix some Python3 issues
This commit is contained in:
ridiculousfish 2012-06-18 13:59:07 -07:00
parent 93dc7d4cc1
commit 9228dffe5e
3 changed files with 165 additions and 59 deletions

View file

@ -1,3 +1,3 @@
function fish_update_completions --description "Update man-page based completions" function fish_update_completions --description "Update man-page based completions"
eval $__fish_datadir/tools/create_manpage_completions.py --manpath --progress eval $__fish_datadir/tools/create_manpage_completions.py --manpath --progress --yield-to $__fish_datadir/completions/
end end

View file

@ -20,9 +20,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
import string, sys, re, os.path, gzip, traceback, getopt, errno import string, sys, re, os.path, gzip, traceback, getopt, errno
from deroff import Deroffer from deroff import Deroffer
# Whether we're Python 3
IS_PY3 = sys.version_info[0] >= 3
# 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 = ""
# Information used to track which of our parsers were successful
PARSER_INFO = {}
# builtcommand writes into this global variable, yuck # builtcommand writes into this global variable, yuck
built_command_output = [] built_command_output = []
@ -34,8 +40,8 @@ diagnostic_indent = 0
VERY_VERBOSE, BRIEF_VERBOSE, NOT_VERBOSE = 2, 1, 0 VERY_VERBOSE, BRIEF_VERBOSE, NOT_VERBOSE = 2, 1, 0
# Pick some reasonable default values for settings # Pick some reasonable default values for settings
global VERBOSITY, WRITE_TO_STDOUT global VERBOSITY, WRITE_TO_STDOUT, DEROFF_ONLY
VERBOSITY, WRITE_TO_STDOUT = NOT_VERBOSE, False VERBOSITY, WRITE_TO_STDOUT, DEROFF_ONLY = NOT_VERBOSE, False, False
def add_diagnostic(dgn, msg_verbosity = VERY_VERBOSE): def add_diagnostic(dgn, msg_verbosity = VERY_VERBOSE):
# Add a diagnostic message, if msg_verbosity <= VERBOSITY # Add a diagnostic message, if msg_verbosity <= VERBOSITY
@ -606,30 +612,35 @@ class TypeDeroffManParser(ManParser):
def name(self): def name(self):
return "Deroffing man parser" return "Deroffing man parser"
# Return whether the file at the given path is overwritable
# Raises IOError if it cannot be opened
def file_is_overwritable(path):
result = False
file = open(path, 'r')
for line in file:
# Skip leading empty lines
line = line.strip()
if not line:
continue
# We look in the initial run of lines that start with #
if not line.startswith('#'):
break
# See if this contains the magic word
if 'Autogenerated' in line:
result = True
break
file.close()
return result
# Return whether the file at the given path either does not exist, or exists but appears to be a file we output (and hence can overwrite) # Return whether the file at the given path either does not exist, or exists but appears to be a file we output (and hence can overwrite)
def file_missing_or_overwritable(path): def file_missing_or_overwritable(path):
try: try:
result = False return file_is_overwritable(path)
file = open(path, 'r')
for line in file:
# Skip leading empty lines
line = line.strip()
if not line:
continue
# We look in the initial run of lines that start with #
if not line.startswith('#'):
break
# See if this contains the magic word
if 'Autogenerated' in line:
result = True
break
file.close()
return result
except IOError as err: except IOError as err:
if err.errno == 2: if err.errno == 2:
# File does not exist, full steam ahead # File does not exist, full steam ahead
@ -637,11 +648,16 @@ def file_missing_or_overwritable(path):
else: else:
# Something else happened # Something else happened
return False return False
# Delete the file if it is autogenerated
def cleanup_autogenerated_file(path):
try:
if file_is_overwritable(path):
os.remove(path)
except (OSError, IOError):
pass
def parse_manpage_at_path(manpage_path, output_directory): def parse_manpage_at_path(manpage_path, yield_to_dirs, output_directory):
filename = os.path.basename(manpage_path) filename = os.path.basename(manpage_path)
# Clear diagnostics # Clear diagnostics
@ -649,23 +665,19 @@ def parse_manpage_at_path(manpage_path, output_directory):
diagnostic_output[:] = [] diagnostic_output[:] = []
diagnostic_indent = 0 diagnostic_indent = 0
# Get the "base" command, e.g. gcc.1.gz -> gcc
global CMDNAME
CMDNAME = filename.split('.', 1)[0]
# Set up some diagnostics # Set up some diagnostics
add_diagnostic('Considering ' + manpage_path) add_diagnostic('Considering ' + manpage_path)
diagnostic_indent += 1 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:
fd = open(manpage_path, 'r')
try: #Utf-8 python3
manpage = fd.read() manpage = fd.read()
except: #Latin-1 python3 if IS_PY3: manpage = manpage.decode('latin-1')
fd = open(manpage_path, 'r', encoding='latin-1') else:
if IS_PY3:
fd = open(manpage_path, 'r', encoding='latin-1')
else:
fd = open(manpage_path, 'r')
manpage = fd.read() manpage = fd.read()
fd.close() fd.close()
@ -689,26 +701,32 @@ def parse_manpage_at_path(manpage_path, output_directory):
# Clear the output list # Clear the output list
built_command_output[:] = [] built_command_output[:] = []
parsers = [Type1ManParser(), Type2ManParser(), Type4ManParser(), Type3ManParser(), TypeDarwinManParser(), TypeDeroffManParser()] if DEROFF_ONLY:
parsers = [TypeDeroffManParser()]
else:
parsers = [Type1ManParser(), Type2ManParser(), Type4ManParser(), Type3ManParser(), TypeDarwinManParser(), TypeDeroffManParser()]
parsersToTry = [p for p in parsers if p.isMyType(manpage)] parsersToTry = [p for p in parsers if p.isMyType(manpage)]
success = False success = False
if not parsersToTry: if not parsersToTry:
add_diagnostic(manpage_path + ": Not supported") add_diagnostic(manpage_path + ": Not supported")
else: else:
for parser in parsersToTry: for parser in parsersToTry:
add_diagnostic('Trying parser ' + parser.name()) parser_name = parser.name()
add_diagnostic('Trying parser ' + parser_name)
diagnostic_indent += 1 diagnostic_indent += 1
success = parser.parseManPage(manpage) success = parser.parseManPage(manpage)
diagnostic_indent -= 1 diagnostic_indent -= 1
if success: break if success:
PARSER_INFO.setdefault(parser_name, []).append(CMDNAME)
break
if success: if success:
if WRITE_TO_STDOUT: if WRITE_TO_STDOUT:
output_file = sys.stdout output_file = sys.stdout
else: else:
fullpath = output_directory + CMDNAME + '.fish' fullpath = os.path.join(output_directory, CMDNAME + '.fish')
try: try:
if file_missing_or_overwritable(fullpath): if file_missing_or_overwritable(fullpath):
output_file = open(fullpath, 'w') output_file = open(fullpath, 'w')
@ -736,8 +754,40 @@ def parse_manpage_at_path(manpage_path, output_directory):
add_diagnostic('%s contains no options or is unparsable (tried parser %s)' % (manpage_path, parser_names), BRIEF_VERBOSE) add_diagnostic('%s contains no options or is unparsable (tried parser %s)' % (manpage_path, parser_names), BRIEF_VERBOSE)
return success return success
def parse_and_output_man_pages(paths, output_directory, show_progress): # Indicates whether the given filename has a presence in one of the yield-to directories
global diagnostic_indent # If so, there's a bespoke completion and we should not generate one
def file_in_yield_directory(filename, yield_to_dirs):
for yield_dir in yield_to_dirs:
test_path = os.path.join(yield_dir, filename)
if os.path.isfile(test_path):
# Yield to the existing file
return True
return False
# Indicates whether we want to skip this command because it already had a non-autogenerated completion
def should_skip_man_page(output_path, filename, yield_to_dirs):
# No reason to skip if we're writing to stdout
if WRITE_TO_STDOUT:
return false
# Check all the yield directories
for yield_dir in yield_to_dirs:
test_path = os.path.join(yield_dir, filename)
if os.path.isfile(test_path):
# Yield to the existing file
return true
# See if there's a hand-written file already
if not file_missing_or_overwritable(output_path):
return true
# We made it through, so don't skip
return false
def parse_and_output_man_pages(paths, output_directory, yield_to_dirs, show_progress):
global diagnostic_indent, CMDNAME
paths.sort() paths.sort()
total_count = len(paths) total_count = len(paths)
successful_count, index = 0, 0 successful_count, index = 0, 0
@ -747,16 +797,41 @@ def parse_and_output_man_pages(paths, output_directory, show_progress):
print("Parsing man pages and writing completions to {0}".format(output_directory)) print("Parsing man pages and writing completions to {0}".format(output_directory))
for manpage_path in paths: for manpage_path in paths:
index += 1 index += 1
# Get the "base" command, e.g. gcc.1.gz -> gcc
man_file_name = os.path.basename(manpage_path)
CMDNAME = man_file_name.split('.', 1)[0]
output_file_name = CMDNAME + '.fish'
# Show progress if we're doing that
if show_progress: if show_progress:
filename = os.path.basename(manpage_path).split('.', 1)[0] progress_str = ' {0} / {1} : {2}'.format((str(index).rjust(padding_len)), total_count, man_file_name)
progress_str = ' {0} / {1} : {2}'.format((str(index).rjust(padding_len)), total_count, filename)
# Pad on the right with spaces so we overwrite whatever we wrote last time # Pad on the right with spaces so we overwrite whatever we wrote last time
padded_progress_str = progress_str.ljust(last_progress_string_length) padded_progress_str = progress_str.ljust(last_progress_string_length)
last_progress_string_length = len(progress_str) last_progress_string_length = len(progress_str)
sys.stdout.write("\r{0} {1}\r".format(padded_progress_str, chr(27))) sys.stdout.write("\r{0} {1}\r".format(padded_progress_str, chr(27)))
sys.stdout.flush(); sys.stdout.flush()
# Maybe we want to skip this item
skip = False
if not WRITE_TO_STDOUT:
# Compute the path that we would write to
output_path = os.path.join(output_directory, output_file_name)
if file_in_yield_directory(output_file_name, yield_to_dirs):
# We're duplicating a bespoke completion - delete any existing completion
skip = True
cleanup_autogenerated_file(output_path)
elif not file_missing_or_overwritable(output_path):
# Don't overwrite a user-created completion
skip = True
# Now skip if requested
if skip:
continue
try: try:
if parse_manpage_at_path(manpage_path, output_directory): if parse_manpage_at_path(manpage_path, yield_to_dirs, output_directory):
successful_count += 1 successful_count += 1
except IOError: except IOError:
diagnostic_indent = 0 diagnostic_indent = 0
@ -799,9 +874,10 @@ def usage(script_name):
print("Usage: {0} [-v, --verbose] [-s, --stdout] [-d, --directory] [-p, --progress] files...".format(script_name)) print("Usage: {0} [-v, --verbose] [-s, --stdout] [-d, --directory] [-p, --progress] files...".format(script_name))
print("""Command options are: print("""Command options are:
-h, --help\t\tShow this help message -h, --help\t\tShow this help message
-v, --verbose\tShow debugging output to stderr -v, --verbose [0, 1, 2]\tShow debugging output to stderr. Larger is more verbose.
-s, --stdout\tWrite all completions to stdout (trumps the --directory option) -s, --stdout\tWrite all completions to stdout (trumps the --directory option)
-d, --directory\tWrite all completions to the given directory, instead of to ~/.config/fish/completions -d, --directory [dir]\tWrite all completions to the given directory, instead of to ~/.config/fish/completions
-y, --yield-to [dir]\tSkip completions that are already present in the given directory
-m, --manpath\tProcess all man1 files available in the manpath (as determined by manpath) -m, --manpath\tProcess all man1 files available in the manpath (as determined by manpath)
-p, --progress\tShow progress -p, --progress\tShow progress
""") """)
@ -809,17 +885,21 @@ def usage(script_name):
if __name__ == "__main__": if __name__ == "__main__":
script_name = sys.argv[0] script_name = sys.argv[0]
try: try:
opts, file_paths = getopt.gnu_getopt(sys.argv[1:], 'vsd:hmp', ['verbose', 'stdout', 'directory=', 'help', 'manpath', 'progress']) opts, file_paths = getopt.gnu_getopt(sys.argv[1:], 'v:sd:hmpy:z', ['verbose=', 'stdout', 'directory=', 'help', 'manpath', 'progress', 'yield-to='])
except getopt.GetoptError as err: except getopt.GetoptError as err:
print(err.strerror) # will print something like "option -a not recognized" print(err.strerror) # will print something like "option -a not recognized"
usage(script_name) usage(script_name)
sys.exit(2) sys.exit(2)
# If a completion already exists in one of the yield-to directories, then don't overwrite it
# And even delete an existing autogenerated one
yield_to_dirs = []
use_manpath, show_progress, custom_dir = False, False, False use_manpath, show_progress, custom_dir = False, False, False
output_directory = '' output_directory = ''
for opt, value in opts: for opt, value in opts:
if opt in ('-v', '--verbose'): if opt in ('-v', '--verbose'):
VERBOSE = True VERBOSITY = int(value)
elif opt in ('-s', '--stdout'): elif opt in ('-s', '--stdout'):
WRITE_TO_STDOUT = True WRITE_TO_STDOUT = True
elif opt in ('-d', '--directory'): elif opt in ('-d', '--directory'):
@ -831,6 +911,12 @@ if __name__ == "__main__":
use_manpath = True use_manpath = True
elif opt in ('-p', '--progress'): elif opt in ('-p', '--progress'):
show_progress = True show_progress = True
elif opt in ('-y', '--yield-to'):
yield_to_dirs.append(value)
if not os.path.isdir(value):
sys.stderr.write("Warning: yield-to directory does not exist: '{0}'\n".format(value))
elif opt in ('-z'):
DEROFF_ONLY = True
else: else:
assert False, "unhandled option" assert False, "unhandled option"
@ -851,12 +937,19 @@ if __name__ == "__main__":
except OSError as e: except OSError as e:
if e.errno != errno.EEXIST: if e.errno != errno.EEXIST:
raise raise
if True: if True:
parse_and_output_man_pages(file_paths, output_directory, show_progress) parse_and_output_man_pages(file_paths, output_directory, yield_to_dirs, show_progress)
else: else:
# Profiling code # Profiling code
import cProfile, pstats import cProfile, pstats
cProfile.run('parse_and_output_man_pages(file_paths, output_directory, show_progress)', 'fooprof') cProfile.run('parse_and_output_man_pages(file_paths, output_directory, yield_to_dirs, show_progress)', 'fooprof')
p = pstats.Stats('fooprof') p = pstats.Stats('fooprof')
p.sort_stats('cumulative').print_stats(100) p.sort_stats('cumulative').print_stats(100)
# Here we can write out all the parser infos
if False:
for name in PARSER_INFO:
print('Parser ' + name + ':')
print('\t' + ', '.join(PARSER_INFO[name]))
print('')

View file

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" Deroff.py, ported to Python from the venerable deroff.c """ """ Deroff.py, ported to Python from the venerable deroff.c """
@ -6,6 +6,8 @@
import sys, re, string import sys, re, string
IS_PY3 = sys.version_info[0] >= 3
class Deroffer: class Deroffer:
g_specs_specletter = { g_specs_specletter = {
@ -307,6 +309,7 @@ class Deroffer:
self.skiplists = False self.skiplists = False
self.ignore_sonx = False self.ignore_sonx = False
self.output = [] self.output = []
self.name = ''
self.OPTIONS = 0 self.OPTIONS = 0
self.FORMAT = 1 self.FORMAT = 1
@ -330,6 +333,7 @@ class Deroffer:
'RB': Deroffer.macro_i_ir, 'RB': Deroffer.macro_i_ir,
'RI': Deroffer.macro_i_ir, 'RI': Deroffer.macro_i_ir,
'AB': Deroffer.macro_i_ir, 'AB': Deroffer.macro_i_ir,
'Nm': Deroffer.macro_Nm,
'] ': Deroffer.macro_close_bracket, '] ': Deroffer.macro_close_bracket,
'PS': Deroffer.macro_ps, 'PS': Deroffer.macro_ps,
'PE': Deroffer.macro_pe, 'PE': Deroffer.macro_pe,
@ -678,6 +682,13 @@ class Deroffer:
pass pass
return False return False
def macro_Nm(self):
if self.s == 'Nm\n':
self.condputs(self.name)
else:
self.name = self.s[3:].strip() + ' '
return True
def macro_close_bracket(self): def macro_close_bracket(self):
self.refer = False self.refer = False
return False return False
@ -1062,12 +1073,14 @@ class Deroffer:
def deroff_files(files): def deroff_files(files):
for arg in files: for arg in files:
print >> sys.stderr, arg sys.stderr.write(arg + '\n')
if arg.endswith('.gz'): if arg.endswith('.gz'):
f = gzip.open(arg, 'r') f = gzip.open(arg, 'r')
str = f.read()
if IS_PY3: str = str.decode('latin-1')
else: else:
f = open(arg, 'r') f = open(arg, 'r')
str = f.read() str = f.read()
d = Deroffer() d = Deroffer()
d.deroff(str) d.deroff(str)
d.flush_output(sys.stdout) d.flush_output(sys.stdout)
@ -1078,7 +1091,7 @@ def deroff_files(files):
if __name__ == "__main__": if __name__ == "__main__":
import gzip import gzip
paths = sys.argv[1:] paths = sys.argv[1:]
if False: if True:
deroff_files(paths) deroff_files(paths)
else: else:
import cProfile, profile, pstats import cProfile, profile, pstats