#!/usr/bin/env python # Copyright (c) 2006, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # 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 # OWNER 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. """gflags2man runs a Google flags base program and generates a man page. Run the program, parse the output, and then format that into a man page. Usage: gflags2man <program> [program] ... """ # TODO(csilvers): work with windows paths (\) as well as unix (/) # This may seem a bit of an end run, but it: doesn't bloat flags, can # support python/java/C++, supports older executables, and can be # extended to other document formats. # Inspired by help2man. import os import re import sys import stat import time import gflags _VERSION = '0.1' def _GetDefaultDestDir(): home = os.environ.get('HOME', '') homeman = os.path.join(home, 'man', 'man1') if home and os.path.exists(homeman): return homeman else: return os.environ.get('TMPDIR', '/tmp') FLAGS = gflags.FLAGS gflags.DEFINE_string('dest_dir', _GetDefaultDestDir(), 'Directory to write resulting manpage to.' ' Specify \'-\' for stdout') gflags.DEFINE_string('help_flag', '--help', 'Option to pass to target program in to get help') gflags.DEFINE_integer('v', 0, 'verbosity level to use for output') _MIN_VALID_USAGE_MSG = 9 # if fewer lines than this, help is suspect class Logging: """A super-simple logging class""" def error(self, msg): print >>sys.stderr, "ERROR: ", msg def warn(self, msg): print >>sys.stderr, "WARNING: ", msg def info(self, msg): print msg def debug(self, msg): self.vlog(1, msg) def vlog(self, level, msg): if FLAGS.v >= level: print msg logging = Logging() class App: def usage(self, shorthelp=0): print >>sys.stderr, __doc__ print >>sys.stderr, "flags:" print >>sys.stderr, str(FLAGS) def run(self): main(sys.argv) app = App() def GetRealPath(filename): """Given an executable filename, find in the PATH or find absolute path. Args: filename An executable filename (string) Returns: Absolute version of filename. None if filename could not be found locally, absolutely, or in PATH """ if os.path.isabs(filename): # already absolute return filename if filename.startswith('./') or filename.startswith('../'): # relative return os.path.abspath(filename) path = os.getenv('PATH', '') for directory in path.split(':'): tryname = os.path.join(directory, filename) if os.path.exists(tryname): if not os.path.isabs(directory): # relative directory return os.path.abspath(tryname) return tryname if os.path.exists(filename): return os.path.abspath(filename) return None # could not determine class Flag(object): """The information about a single flag.""" def __init__(self, flag_desc, help): """Create the flag object. Args: flag_desc The command line forms this could take. (string) help The help text (string) """ self.desc = flag_desc # the command line forms self.help = help # the help text self.default = '' # default value self.tips = '' # parsing/syntax tips class ProgramInfo(object): """All the information gleaned from running a program with --help.""" # Match a module block start, for python scripts --help # "goopy.logging:" module_py_re = re.compile(r'(\S.+):$') # match the start of a flag listing # " -v,--verbosity: Logging verbosity" flag_py_re = re.compile(r'\s+(-\S+):\s+(.*)$') # " (default: '0')" flag_default_py_re = re.compile(r'\s+\(default:\s+\'(.*)\'\)$') # " (an integer)" flag_tips_py_re = re.compile(r'\s+\((.*)\)$') # Match a module block start, for c++ programs --help # "google/base/commandlineflags": module_c_re = re.compile(r'\s+Flags from (\S.+):$') # match the start of a flag listing # " -v,--verbosity: Logging verbosity" flag_c_re = re.compile(r'\s+(-\S+)\s+(.*)$') # Match a module block start, for java programs --help # "com.google.common.flags" module_java_re = re.compile(r'\s+Flags for (\S.+):$') # match the start of a flag listing # " -v,--verbosity: Logging verbosity" flag_java_re = re.compile(r'\s+(-\S+)\s+(.*)$') def __init__(self, executable): """Create object with executable. Args: executable Program to execute (string) """ self.long_name = executable self.name = os.path.basename(executable) # name # Get name without extension (PAR files) (self.short_name, self.ext) = os.path.splitext(self.name) self.executable = GetRealPath(executable) # name of the program self.output = [] # output from the program. List of lines. self.desc = [] # top level description. List of lines self.modules = {} # { section_name(string), [ flags ] } self.module_list = [] # list of module names in their original order self.date = time.localtime(time.time()) # default date info def Run(self): """Run it and collect output. Returns: 1 (true) If everything went well. 0 (false) If there were problems. """ if not self.executable: logging.error('Could not locate "%s"' % self.long_name) return 0 finfo = os.stat(self.executable) self.date = time.localtime(finfo[stat.ST_MTIME]) logging.info('Running: %s %s </dev/null 2>&1' % (self.executable, FLAGS.help_flag)) # --help output is often routed to stderr, so we combine with stdout. # Re-direct stdin to /dev/null to encourage programs that # don't understand --help to exit. (child_stdin, child_stdout_and_stderr) = os.popen4( [self.executable, FLAGS.help_flag]) child_stdin.close() # '</dev/null' self.output = child_stdout_and_stderr.readlines() child_stdout_and_stderr.close() if len(self.output) < _MIN_VALID_USAGE_MSG: logging.error('Error: "%s %s" returned only %d lines: %s' % (self.name, FLAGS.help_flag, len(self.output), self.output)) return 0 return 1 def Parse(self): """Parse program output.""" (start_line, lang) = self.ParseDesc() if start_line < 0: return if 'python' == lang: self.ParsePythonFlags(start_line) elif 'c' == lang: self.ParseCFlags(start_line) elif 'java' == lang: self.ParseJavaFlags(start_line) def ParseDesc(self, start_line=0): """Parse the initial description. This could be Python or C++. Returns: (start_line, lang_type) start_line Line to start parsing flags on (int) lang_type Either 'python' or 'c' (-1, '') if the flags start could not be found """ exec_mod_start = self.executable + ':' after_blank = 0 start_line = 0 # ignore the passed-in arg for now (?) for start_line in range(start_line, len(self.output)): # collect top description line = self.output[start_line].rstrip() # Python flags start with 'flags:\n' if ('flags:' == line and len(self.output) > start_line+1 and '' == self.output[start_line+1].rstrip()): start_line += 2 logging.debug('Flags start (python): %s' % line) return (start_line, 'python') # SWIG flags just have the module name followed by colon. if exec_mod_start == line: logging.debug('Flags start (swig): %s' % line) return (start_line, 'python') # C++ flags begin after a blank line and with a constant string if after_blank and line.startswith(' Flags from '): logging.debug('Flags start (c): %s' % line) return (start_line, 'c') # java flags begin with a constant string if line == 'where flags are': logging.debug('Flags start (java): %s' % line) start_line += 2 # skip "Standard flags:" return (start_line, 'java') logging.debug('Desc: %s' % line) self.desc.append(line) after_blank = (line == '') else: logging.warn('Never found the start of the flags section for "%s"!' % self.long_name) return (-1, '') def ParsePythonFlags(self, start_line=0): """Parse python/swig style flags.""" modname = None # name of current module modlist = [] flag = None for line_num in range(start_line, len(self.output)): # collect flags line = self.output[line_num].rstrip() if not line: # blank continue mobj = self.module_py_re.match(line) if mobj: # start of a new module modname = mobj.group(1) logging.debug('Module: %s' % line) if flag: modlist.append(flag) self.module_list.append(modname) self.modules.setdefault(modname, []) modlist = self.modules[modname] flag = None continue mobj = self.flag_py_re.match(line) if mobj: # start of a new flag if flag: modlist.append(flag) logging.debug('Flag: %s' % line) flag = Flag(mobj.group(1), mobj.group(2)) continue if not flag: # continuation of a flag logging.error('Flag info, but no current flag "%s"' % line) mobj = self.flag_default_py_re.match(line) if mobj: # (default: '...') flag.default = mobj.group(1) logging.debug('Fdef: %s' % line) continue mobj = self.flag_tips_py_re.match(line) if mobj: # (tips) flag.tips = mobj.group(1) logging.debug('Ftip: %s' % line) continue if flag and flag.help: flag.help += line # multiflags tack on an extra line else: logging.info('Extra: %s' % line) if flag: modlist.append(flag) def ParseCFlags(self, start_line=0): """Parse C style flags.""" modname = None # name of current module modlist = [] flag = None for line_num in range(start_line, len(self.output)): # collect flags line = self.output[line_num].rstrip() if not line: # blank lines terminate flags if flag: # save last flag modlist.append(flag) flag = None continue mobj = self.module_c_re.match(line) if mobj: # start of a new module modname = mobj.group(1) logging.debug('Module: %s' % line) if flag: modlist.append(flag) self.module_list.append(modname) self.modules.setdefault(modname, []) modlist = self.modules[modname] flag = None continue mobj = self.flag_c_re.match(line) if mobj: # start of a new flag if flag: # save last flag modlist.append(flag) logging.debug('Flag: %s' % line) flag = Flag(mobj.group(1), mobj.group(2)) continue # append to flag help. type and default are part of the main text if flag: flag.help += ' ' + line.strip() else: logging.info('Extra: %s' % line) if flag: modlist.append(flag) def ParseJavaFlags(self, start_line=0): """Parse Java style flags (com.google.common.flags).""" # The java flags prints starts with a "Standard flags" "module" # that doesn't follow the standard module syntax. modname = 'Standard flags' # name of current module self.module_list.append(modname) self.modules.setdefault(modname, []) modlist = self.modules[modname] flag = None for line_num in range(start_line, len(self.output)): # collect flags line = self.output[line_num].rstrip() logging.vlog(2, 'Line: "%s"' % line) if not line: # blank lines terminate module if flag: # save last flag modlist.append(flag) flag = None continue mobj = self.module_java_re.match(line) if mobj: # start of a new module modname = mobj.group(1) logging.debug('Module: %s' % line) if flag: modlist.append(flag) self.module_list.append(modname) self.modules.setdefault(modname, []) modlist = self.modules[modname] flag = None continue mobj = self.flag_java_re.match(line) if mobj: # start of a new flag if flag: # save last flag modlist.append(flag) logging.debug('Flag: %s' % line) flag = Flag(mobj.group(1), mobj.group(2)) continue # append to flag help. type and default are part of the main text if flag: flag.help += ' ' + line.strip() else: logging.info('Extra: %s' % line) if flag: modlist.append(flag) def Filter(self): """Filter parsed data to create derived fields.""" if not self.desc: self.short_desc = '' return for i in range(len(self.desc)): # replace full path with name if self.desc[i].find(self.executable) >= 0: self.desc[i] = self.desc[i].replace(self.executable, self.name) self.short_desc = self.desc[0] word_list = self.short_desc.split(' ') all_names = [ self.name, self.short_name, ] # Since the short_desc is always listed right after the name, # trim it from the short_desc while word_list and (word_list[0] in all_names or word_list[0].lower() in all_names): del word_list[0] self.short_desc = '' # signal need to reconstruct if not self.short_desc and word_list: self.short_desc = ' '.join(word_list) class GenerateDoc(object): """Base class to output flags information.""" def __init__(self, proginfo, directory='.'): """Create base object. Args: proginfo A ProgramInfo object directory Directory to write output into """ self.info = proginfo self.dirname = directory def Output(self): """Output all sections of the page.""" self.Open() self.Header() self.Body() self.Footer() def Open(self): raise NotImplementedError # define in subclass def Header(self): raise NotImplementedError # define in subclass def Body(self): raise NotImplementedError # define in subclass def Footer(self): raise NotImplementedError # define in subclass class GenerateMan(GenerateDoc): """Output a man page.""" def __init__(self, proginfo, directory='.'): """Create base object. Args: proginfo A ProgramInfo object directory Directory to write output into """ GenerateDoc.__init__(self, proginfo, directory) def Open(self): if self.dirname == '-': logging.info('Writing to stdout') self.fp = sys.stdout else: self.file_path = '%s.1' % os.path.join(self.dirname, self.info.name) logging.info('Writing: %s' % self.file_path) self.fp = open(self.file_path, 'w') def Header(self): self.fp.write( '.\\" DO NOT MODIFY THIS FILE! It was generated by gflags2man %s\n' % _VERSION) self.fp.write( '.TH %s "1" "%s" "%s" "User Commands"\n' % (self.info.name, time.strftime('%x', self.info.date), self.info.name)) self.fp.write( '.SH NAME\n%s \\- %s\n' % (self.info.name, self.info.short_desc)) self.fp.write( '.SH SYNOPSIS\n.B %s\n[\\fIFLAGS\\fR]...\n' % self.info.name) def Body(self): self.fp.write( '.SH DESCRIPTION\n.\\" Add any additional description here\n.PP\n') for ln in self.info.desc: self.fp.write('%s\n' % ln) self.fp.write( '.SH OPTIONS\n') # This shows flags in the original order for modname in self.info.module_list: if modname.find(self.info.executable) >= 0: mod = modname.replace(self.info.executable, self.info.name) else: mod = modname self.fp.write('\n.P\n.I %s\n' % mod) for flag in self.info.modules[modname]: help_string = flag.help if flag.default or flag.tips: help_string += '\n.br\n' if flag.default: help_string += ' (default: \'%s\')' % flag.default if flag.tips: help_string += ' (%s)' % flag.tips self.fp.write( '.TP\n%s\n%s\n' % (flag.desc, help_string)) def Footer(self): self.fp.write( '.SH COPYRIGHT\nCopyright \(co %s Google.\n' % time.strftime('%Y', self.info.date)) self.fp.write('Gflags2man created this page from "%s %s" output.\n' % (self.info.name, FLAGS.help_flag)) self.fp.write('\nGflags2man was written by Dan Christian. ' ' Note that the date on this' ' page is the modification date of %s.\n' % self.info.name) def main(argv): argv = FLAGS(argv) # handles help as well if len(argv) <= 1: app.usage(shorthelp=1) return 1 for arg in argv[1:]: prog = ProgramInfo(arg) if not prog.Run(): continue prog.Parse() prog.Filter() doc = GenerateMan(prog, FLAGS.dest_dir) doc.Output() return 0 if __name__ == '__main__': app.run()