Home | History | Annotate | Download | only in python_gflags
      1 #!/usr/bin/env python
      2 
      3 # Copyright (c) 2006, Google Inc.
      4 # All rights reserved.
      5 #
      6 # Redistribution and use in source and binary forms, with or without
      7 # modification, are permitted provided that the following conditions are
      8 # met:
      9 #
     10 #     * Redistributions of source code must retain the above copyright
     11 # notice, this list of conditions and the following disclaimer.
     12 #     * Redistributions in binary form must reproduce the above
     13 # copyright notice, this list of conditions and the following disclaimer
     14 # in the documentation and/or other materials provided with the
     15 # distribution.
     16 #     * Neither the name of Google Inc. nor the names of its
     17 # contributors may be used to endorse or promote products derived from
     18 # this software without specific prior written permission.
     19 #
     20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31 
     32 
     33 """gflags2man runs a Google flags base program and generates a man page.
     34 
     35 Run the program, parse the output, and then format that into a man
     36 page.
     37 
     38 Usage:
     39   gflags2man <program> [program] ...
     40 """
     41 
     42 # TODO(csilvers): work with windows paths (\) as well as unix (/)
     43 
     44 # This may seem a bit of an end run, but it:  doesn't bloat flags, can
     45 # support python/java/C++, supports older executables, and can be
     46 # extended to other document formats.
     47 # Inspired by help2man.
     48 
     49 
     50 
     51 import os
     52 import re
     53 import sys
     54 import stat
     55 import time
     56 
     57 import gflags
     58 
     59 _VERSION = '0.1'
     60 
     61 
     62 def _GetDefaultDestDir():
     63   home = os.environ.get('HOME', '')
     64   homeman = os.path.join(home, 'man', 'man1')
     65   if home and os.path.exists(homeman):
     66     return homeman
     67   else:
     68     return os.environ.get('TMPDIR', '/tmp')
     69 
     70 FLAGS = gflags.FLAGS
     71 gflags.DEFINE_string('dest_dir', _GetDefaultDestDir(),
     72                     'Directory to write resulting manpage to.'
     73                     ' Specify \'-\' for stdout')
     74 gflags.DEFINE_string('help_flag', '--help',
     75                     'Option to pass to target program in to get help')
     76 gflags.DEFINE_integer('v', 0, 'verbosity level to use for output')
     77 
     78 
     79 _MIN_VALID_USAGE_MSG = 9         # if fewer lines than this, help is suspect
     80 
     81 
     82 class Logging:
     83   """A super-simple logging class"""
     84   def error(self, msg): print >>sys.stderr, "ERROR: ", msg
     85   def warn(self, msg): print >>sys.stderr, "WARNING: ", msg
     86   def info(self, msg): print msg
     87   def debug(self, msg): self.vlog(1, msg)
     88   def vlog(self, level, msg):
     89     if FLAGS.v >= level: print msg
     90 logging = Logging()
     91 class App:
     92   def usage(self, shorthelp=0):
     93     print >>sys.stderr, __doc__
     94     print >>sys.stderr, "flags:"
     95     print >>sys.stderr, str(FLAGS)
     96   def run(self):
     97     main(sys.argv)
     98 app = App()
     99 
    100 
    101 def GetRealPath(filename):
    102   """Given an executable filename, find in the PATH or find absolute path.
    103   Args:
    104     filename  An executable filename (string)
    105   Returns:
    106     Absolute version of filename.
    107     None if filename could not be found locally, absolutely, or in PATH
    108   """
    109   if os.path.isabs(filename):                # already absolute
    110     return filename
    111 
    112   if filename.startswith('./') or  filename.startswith('../'): # relative
    113     return os.path.abspath(filename)
    114 
    115   path = os.getenv('PATH', '')
    116   for directory in path.split(':'):
    117     tryname = os.path.join(directory, filename)
    118     if os.path.exists(tryname):
    119       if not os.path.isabs(directory):  # relative directory
    120         return os.path.abspath(tryname)
    121       return tryname
    122   if os.path.exists(filename):
    123     return os.path.abspath(filename)
    124   return None                         # could not determine
    125 
    126 class Flag(object):
    127   """The information about a single flag."""
    128 
    129   def __init__(self, flag_desc, help):
    130     """Create the flag object.
    131     Args:
    132       flag_desc  The command line forms this could take. (string)
    133       help       The help text (string)
    134     """
    135     self.desc = flag_desc               # the command line forms
    136     self.help = help                    # the help text
    137     self.default = ''                   # default value
    138     self.tips = ''                      # parsing/syntax tips
    139 
    140 
    141 class ProgramInfo(object):
    142   """All the information gleaned from running a program with --help."""
    143 
    144   # Match a module block start, for python scripts --help
    145   # "goopy.logging:"
    146   module_py_re = re.compile(r'(\S.+):$')
    147   # match the start of a flag listing
    148   # " -v,--verbosity:  Logging verbosity"
    149   flag_py_re         = re.compile(r'\s+(-\S+):\s+(.*)$')
    150   # "   (default: '0')"
    151   flag_default_py_re = re.compile(r'\s+\(default:\s+\'(.*)\'\)$')
    152   # "   (an integer)"
    153   flag_tips_py_re    = re.compile(r'\s+\((.*)\)$')
    154 
    155   # Match a module block start, for c++ programs --help
    156   # "google/base/commandlineflags":
    157   module_c_re = re.compile(r'\s+Flags from (\S.+):$')
    158   # match the start of a flag listing
    159   # " -v,--verbosity:  Logging verbosity"
    160   flag_c_re         = re.compile(r'\s+(-\S+)\s+(.*)$')
    161 
    162   # Match a module block start, for java programs --help
    163   # "com.google.common.flags"
    164   module_java_re = re.compile(r'\s+Flags for (\S.+):$')
    165   # match the start of a flag listing
    166   # " -v,--verbosity:  Logging verbosity"
    167   flag_java_re         = re.compile(r'\s+(-\S+)\s+(.*)$')
    168 
    169   def __init__(self, executable):
    170     """Create object with executable.
    171     Args:
    172       executable  Program to execute (string)
    173     """
    174     self.long_name = executable
    175     self.name = os.path.basename(executable)  # name
    176     # Get name without extension (PAR files)
    177     (self.short_name, self.ext) = os.path.splitext(self.name)
    178     self.executable = GetRealPath(executable)  # name of the program
    179     self.output = []          # output from the program.  List of lines.
    180     self.desc = []            # top level description.  List of lines
    181     self.modules = {}         # { section_name(string), [ flags ] }
    182     self.module_list = []     # list of module names in their original order
    183     self.date = time.localtime(time.time())   # default date info
    184 
    185   def Run(self):
    186     """Run it and collect output.
    187 
    188     Returns:
    189       1 (true)   If everything went well.
    190       0 (false)  If there were problems.
    191     """
    192     if not self.executable:
    193       logging.error('Could not locate "%s"' % self.long_name)
    194       return 0
    195 
    196     finfo = os.stat(self.executable)
    197     self.date = time.localtime(finfo[stat.ST_MTIME])
    198 
    199     logging.info('Running: %s %s </dev/null 2>&1'
    200                  % (self.executable, FLAGS.help_flag))
    201     # --help output is often routed to stderr, so we combine with stdout.
    202     # Re-direct stdin to /dev/null to encourage programs that
    203     # don't understand --help to exit.
    204     (child_stdin, child_stdout_and_stderr) = os.popen4(
    205       [self.executable, FLAGS.help_flag])
    206     child_stdin.close()       # '</dev/null'
    207     self.output = child_stdout_and_stderr.readlines()
    208     child_stdout_and_stderr.close()
    209     if len(self.output) < _MIN_VALID_USAGE_MSG:
    210       logging.error('Error: "%s %s" returned only %d lines: %s'
    211                     % (self.name, FLAGS.help_flag,
    212                        len(self.output), self.output))
    213       return 0
    214     return 1
    215 
    216   def Parse(self):
    217     """Parse program output."""
    218     (start_line, lang) = self.ParseDesc()
    219     if start_line < 0:
    220       return
    221     if 'python' == lang:
    222       self.ParsePythonFlags(start_line)
    223     elif 'c' == lang:
    224       self.ParseCFlags(start_line)
    225     elif 'java' == lang:
    226       self.ParseJavaFlags(start_line)
    227 
    228   def ParseDesc(self, start_line=0):
    229     """Parse the initial description.
    230 
    231     This could be Python or C++.
    232 
    233     Returns:
    234       (start_line, lang_type)
    235         start_line  Line to start parsing flags on (int)
    236         lang_type   Either 'python' or 'c'
    237        (-1, '')  if the flags start could not be found
    238     """
    239     exec_mod_start = self.executable + ':'
    240 
    241     after_blank = 0
    242     start_line = 0             # ignore the passed-in arg for now (?)
    243     for start_line in range(start_line, len(self.output)): # collect top description
    244       line = self.output[start_line].rstrip()
    245       # Python flags start with 'flags:\n'
    246       if ('flags:' == line
    247           and len(self.output) > start_line+1
    248           and '' == self.output[start_line+1].rstrip()):
    249         start_line += 2
    250         logging.debug('Flags start (python): %s' % line)
    251         return (start_line, 'python')
    252       # SWIG flags just have the module name followed by colon.
    253       if exec_mod_start == line:
    254         logging.debug('Flags start (swig): %s' % line)
    255         return (start_line, 'python')
    256       # C++ flags begin after a blank line and with a constant string
    257       if after_blank and line.startswith('  Flags from '):
    258         logging.debug('Flags start (c): %s' % line)
    259         return (start_line, 'c')
    260       # java flags begin with a constant string
    261       if line == 'where flags are':
    262         logging.debug('Flags start (java): %s' % line)
    263         start_line += 2                        # skip "Standard flags:"
    264         return (start_line, 'java')
    265 
    266       logging.debug('Desc: %s' % line)
    267       self.desc.append(line)
    268       after_blank = (line == '')
    269     else:
    270       logging.warn('Never found the start of the flags section for "%s"!'
    271                    % self.long_name)
    272       return (-1, '')
    273 
    274   def ParsePythonFlags(self, start_line=0):
    275     """Parse python/swig style flags."""
    276     modname = None                      # name of current module
    277     modlist = []
    278     flag = None
    279     for line_num in range(start_line, len(self.output)): # collect flags
    280       line = self.output[line_num].rstrip()
    281       if not line:                      # blank
    282         continue
    283 
    284       mobj = self.module_py_re.match(line)
    285       if mobj:                          # start of a new module
    286         modname = mobj.group(1)
    287         logging.debug('Module: %s' % line)
    288         if flag:
    289           modlist.append(flag)
    290         self.module_list.append(modname)
    291         self.modules.setdefault(modname, [])
    292         modlist = self.modules[modname]
    293         flag = None
    294         continue
    295 
    296       mobj = self.flag_py_re.match(line)
    297       if mobj:                          # start of a new flag
    298         if flag:
    299           modlist.append(flag)
    300         logging.debug('Flag: %s' % line)
    301         flag = Flag(mobj.group(1),  mobj.group(2))
    302         continue
    303 
    304       if not flag:                    # continuation of a flag
    305         logging.error('Flag info, but no current flag "%s"' % line)
    306       mobj = self.flag_default_py_re.match(line)
    307       if mobj:                          # (default: '...')
    308         flag.default = mobj.group(1)
    309         logging.debug('Fdef: %s' % line)
    310         continue
    311       mobj = self.flag_tips_py_re.match(line)
    312       if mobj:                          # (tips)
    313         flag.tips = mobj.group(1)
    314         logging.debug('Ftip: %s' % line)
    315         continue
    316       if flag and flag.help:
    317         flag.help += line              # multiflags tack on an extra line
    318       else:
    319         logging.info('Extra: %s' % line)
    320     if flag:
    321       modlist.append(flag)
    322 
    323   def ParseCFlags(self, start_line=0):
    324     """Parse C style flags."""
    325     modname = None                      # name of current module
    326     modlist = []
    327     flag = None
    328     for line_num in range(start_line, len(self.output)):  # collect flags
    329       line = self.output[line_num].rstrip()
    330       if not line:                      # blank lines terminate flags
    331         if flag:                        # save last flag
    332           modlist.append(flag)
    333           flag = None
    334         continue
    335 
    336       mobj = self.module_c_re.match(line)
    337       if mobj:                          # start of a new module
    338         modname = mobj.group(1)
    339         logging.debug('Module: %s' % line)
    340         if flag:
    341           modlist.append(flag)
    342         self.module_list.append(modname)
    343         self.modules.setdefault(modname, [])
    344         modlist = self.modules[modname]
    345         flag = None
    346         continue
    347 
    348       mobj = self.flag_c_re.match(line)
    349       if mobj:                          # start of a new flag
    350         if flag:                        # save last flag
    351           modlist.append(flag)
    352         logging.debug('Flag: %s' % line)
    353         flag = Flag(mobj.group(1),  mobj.group(2))
    354         continue
    355 
    356       # append to flag help.  type and default are part of the main text
    357       if flag:
    358         flag.help += ' ' + line.strip()
    359       else:
    360         logging.info('Extra: %s' % line)
    361     if flag:
    362       modlist.append(flag)
    363 
    364   def ParseJavaFlags(self, start_line=0):
    365     """Parse Java style flags (com.google.common.flags)."""
    366     # The java flags prints starts with a "Standard flags" "module"
    367     # that doesn't follow the standard module syntax.
    368     modname = 'Standard flags'          # name of current module
    369     self.module_list.append(modname)
    370     self.modules.setdefault(modname, [])
    371     modlist = self.modules[modname]
    372     flag = None
    373 
    374     for line_num in range(start_line, len(self.output)): # collect flags
    375       line = self.output[line_num].rstrip()
    376       logging.vlog(2, 'Line: "%s"' % line)
    377       if not line:                      # blank lines terminate module
    378         if flag:                        # save last flag
    379           modlist.append(flag)
    380           flag = None
    381         continue
    382 
    383       mobj = self.module_java_re.match(line)
    384       if mobj:                          # start of a new module
    385         modname = mobj.group(1)
    386         logging.debug('Module: %s' % line)
    387         if flag:
    388           modlist.append(flag)
    389         self.module_list.append(modname)
    390         self.modules.setdefault(modname, [])
    391         modlist = self.modules[modname]
    392         flag = None
    393         continue
    394 
    395       mobj = self.flag_java_re.match(line)
    396       if mobj:                          # start of a new flag
    397         if flag:                        # save last flag
    398           modlist.append(flag)
    399         logging.debug('Flag: %s' % line)
    400         flag = Flag(mobj.group(1),  mobj.group(2))
    401         continue
    402 
    403       # append to flag help.  type and default are part of the main text
    404       if flag:
    405         flag.help += ' ' + line.strip()
    406       else:
    407         logging.info('Extra: %s' % line)
    408     if flag:
    409       modlist.append(flag)
    410 
    411   def Filter(self):
    412     """Filter parsed data to create derived fields."""
    413     if not self.desc:
    414       self.short_desc = ''
    415       return
    416 
    417     for i in range(len(self.desc)):   # replace full path with name
    418       if self.desc[i].find(self.executable) >= 0:
    419         self.desc[i] = self.desc[i].replace(self.executable, self.name)
    420 
    421     self.short_desc = self.desc[0]
    422     word_list = self.short_desc.split(' ')
    423     all_names = [ self.name, self.short_name, ]
    424     # Since the short_desc is always listed right after the name,
    425     #  trim it from the short_desc
    426     while word_list and (word_list[0] in all_names
    427                          or word_list[0].lower() in all_names):
    428       del word_list[0]
    429       self.short_desc = ''              # signal need to reconstruct
    430     if not self.short_desc and word_list:
    431       self.short_desc = ' '.join(word_list)
    432 
    433 
    434 class GenerateDoc(object):
    435   """Base class to output flags information."""
    436 
    437   def __init__(self, proginfo, directory='.'):
    438     """Create base object.
    439     Args:
    440       proginfo   A ProgramInfo object
    441       directory  Directory to write output into
    442     """
    443     self.info = proginfo
    444     self.dirname = directory
    445 
    446   def Output(self):
    447     """Output all sections of the page."""
    448     self.Open()
    449     self.Header()
    450     self.Body()
    451     self.Footer()
    452 
    453   def Open(self): raise NotImplementedError    # define in subclass
    454   def Header(self): raise NotImplementedError  # define in subclass
    455   def Body(self): raise NotImplementedError    # define in subclass
    456   def Footer(self): raise NotImplementedError  # define in subclass
    457 
    458 
    459 class GenerateMan(GenerateDoc):
    460   """Output a man page."""
    461 
    462   def __init__(self, proginfo, directory='.'):
    463     """Create base object.
    464     Args:
    465       proginfo   A ProgramInfo object
    466       directory  Directory to write output into
    467     """
    468     GenerateDoc.__init__(self, proginfo, directory)
    469 
    470   def Open(self):
    471     if self.dirname == '-':
    472       logging.info('Writing to stdout')
    473       self.fp = sys.stdout
    474     else:
    475       self.file_path = '%s.1' % os.path.join(self.dirname, self.info.name)
    476       logging.info('Writing: %s' % self.file_path)
    477       self.fp = open(self.file_path, 'w')
    478 
    479   def Header(self):
    480     self.fp.write(
    481       '.\\" DO NOT MODIFY THIS FILE!  It was generated by gflags2man %s\n'
    482       % _VERSION)
    483     self.fp.write(
    484       '.TH %s "1" "%s" "%s" "User Commands"\n'
    485       % (self.info.name, time.strftime('%x', self.info.date), self.info.name))
    486     self.fp.write(
    487       '.SH NAME\n%s \\- %s\n' % (self.info.name, self.info.short_desc))
    488     self.fp.write(
    489       '.SH SYNOPSIS\n.B %s\n[\\fIFLAGS\\fR]...\n' % self.info.name)
    490 
    491   def Body(self):
    492     self.fp.write(
    493       '.SH DESCRIPTION\n.\\" Add any additional description here\n.PP\n')
    494     for ln in self.info.desc:
    495       self.fp.write('%s\n' % ln)
    496     self.fp.write(
    497       '.SH OPTIONS\n')
    498     # This shows flags in the original order
    499     for modname in self.info.module_list:
    500       if modname.find(self.info.executable) >= 0:
    501         mod = modname.replace(self.info.executable, self.info.name)
    502       else:
    503         mod = modname
    504       self.fp.write('\n.P\n.I %s\n' % mod)
    505       for flag in self.info.modules[modname]:
    506         help_string = flag.help
    507         if flag.default or flag.tips:
    508           help_string += '\n.br\n'
    509         if flag.default:
    510           help_string += '  (default: \'%s\')' % flag.default
    511         if flag.tips:
    512           help_string += '  (%s)' % flag.tips
    513         self.fp.write(
    514           '.TP\n%s\n%s\n' % (flag.desc, help_string))
    515 
    516   def Footer(self):
    517     self.fp.write(
    518       '.SH COPYRIGHT\nCopyright \(co %s Google.\n'
    519       % time.strftime('%Y', self.info.date))
    520     self.fp.write('Gflags2man created this page from "%s %s" output.\n'
    521                   % (self.info.name, FLAGS.help_flag))
    522     self.fp.write('\nGflags2man was written by Dan Christian. '
    523                   ' Note that the date on this'
    524                   ' page is the modification date of %s.\n' % self.info.name)
    525 
    526 
    527 def main(argv):
    528   argv = FLAGS(argv)           # handles help as well
    529   if len(argv) <= 1:
    530     app.usage(shorthelp=1)
    531     return 1
    532 
    533   for arg in argv[1:]:
    534     prog = ProgramInfo(arg)
    535     if not prog.Run():
    536       continue
    537     prog.Parse()
    538     prog.Filter()
    539     doc = GenerateMan(prog, FLAGS.dest_dir)
    540     doc.Output()
    541   return 0
    542 
    543 if __name__ == '__main__':
    544   app.run()
    545