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
35Run the program, parse the output, and then format that into a man
36page.
37
38Usage:
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
51import os
52import re
53import sys
54import stat
55import time
56
57import gflags
58
59_VERSION = '0.1'
60
61
62def _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
70FLAGS = gflags.FLAGS
71gflags.DEFINE_string('dest_dir', _GetDefaultDestDir(),
72                    'Directory to write resulting manpage to.'
73                    ' Specify \'-\' for stdout')
74gflags.DEFINE_string('help_flag', '--help',
75                    'Option to pass to target program in to get help')
76gflags.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
82class 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
90logging = Logging()
91class 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)
98app = App()
99
100
101def 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
126class 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
141class 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
434class 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
459class 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
527def 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
543if __name__ == '__main__':
544  app.run()
545