| #!/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() |