3
# Copyright (c) 2007, Google Inc.
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are
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
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.
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.
32
"""gflags2man runs a Google flags base program and generates a man page.
34
Run the program, parse the output, and then format that into a man
38
gflags2man <program> [program] ...
41
# TODO(csilvers): work with windows paths (\) as well as unix (/)
43
# This may seem a bit of an end run, but it: doesn't bloat flags, can
44
# support python/java/C++, supports older executables, and can be
45
# extended to other document formats.
46
# Inspired by help2man.
48
__author__ = 'Dan Christian'
61
def _GetDefaultDestDir():
62
home = os.environ.get('HOME', '')
63
homeman = os.path.join(home, 'man', 'man1')
64
if home and os.path.exists(homeman):
67
return os.environ.get('TMPDIR', '/tmp')
70
gflags.DEFINE_string('dest_dir', _GetDefaultDestDir(),
71
'Directory to write resulting manpage to.'
72
' Specify \'-\' for stdout')
73
gflags.DEFINE_string('help_flag', '--help',
74
'Option to pass to target program in to get help')
75
gflags.DEFINE_integer('v', 0, 'verbosity level to use for output')
77
_MIN_VALID_USAGE_MSG = 9 # if fewer lines than this, help is suspect
81
"""A super-simple logging class"""
82
def error(self, msg): print >>sys.stderr, "ERROR: ", msg
83
def warn(self, msg): print >>sys.stderr, "WARNING: ", msg
84
def info(self, msg): print msg
85
def debug(self, msg): self.vlog(1, msg)
86
def vlog(self, level, msg):
87
if FLAGS.v >= level: print msg
91
def GetRealPath(filename):
92
"""Given an executable filename, find in the PATH or find absolute path.
94
filename An executable filename (string)
96
Absolute version of filename.
97
None if filename could not be found locally, absolutely, or in PATH
99
if os.path.isabs(filename): # already absolute
102
if filename.startswith('./') or filename.startswith('../'): # relative
103
return os.path.abspath(filename)
105
path = os.getenv('PATH', '')
106
for directory in path.split(':'):
107
tryname = os.path.join(directory, filename)
108
if os.path.exists(tryname):
109
if not os.path.isabs(directory): # relative directory
110
return os.path.abspath(tryname)
112
if os.path.exists(filename):
113
return os.path.abspath(filename)
114
return None # could not determine
117
"""The information about a single flag."""
119
def __init__(self, flag_desc, help):
120
"""Create the flag object.
122
flag_desc The command line forms this could take. (string)
123
help The help text (string)
125
self.desc = flag_desc # the command line forms
126
self.help = help # the help text
127
self.default = '' # default value
128
self.tips = '' # parsing/syntax tips
131
class ProgramInfo(object):
132
"""All the information gleaned from running a program with --help."""
134
# Match a module block start, for python scripts --help
136
module_py_re = re.compile(r'(\S.+):$')
137
# match the start of a flag listing
138
# " -v,--verbosity: Logging verbosity"
139
flag_py_re = re.compile(r'\s+(-\S+):\s+(.*)$')
141
flag_default_py_re = re.compile(r'\s+\(default:\s+\'(.*)\'\)$')
143
flag_tips_py_re = re.compile(r'\s+\((.*)\)$')
145
# Match a module block start, for c++ programs --help
146
# "google/base/commandlineflags"
147
module_c_re = re.compile(r'\s+Flags from (\S.+):$')
148
# match the start of a flag listing
149
# " -v,--verbosity: Logging verbosity"
150
flag_c_re = re.compile(r'\s+(-\S+)\s+(.*)$')
152
# Match a module block start, for java programs --help
153
# "com.google.common.flags"
154
module_java_re = re.compile(r'\s+Flags for (\S.+):$')
155
# match the start of a flag listing
156
# " -v,--verbosity: Logging verbosity"
157
flag_java_re = re.compile(r'\s+(-\S+)\s+(.*)$')
159
def __init__(self, executable):
160
"""Create object with executable.
162
executable Program to execute (string)
164
self.long_name = executable
165
self.name = os.path.basename(executable) # name
166
# Get name without extension (PAR files)
167
(self.short_name, self.ext) = os.path.splitext(self.name)
168
self.executable = GetRealPath(executable) # name of the program
169
self.output = [] # output from the program. List of lines.
170
self.desc = [] # top level description. List of lines
171
self.modules = {} # { section_name(string), [ flags ] }
172
self.module_list = [] # list of module names in their original order
173
self.date = time.localtime(time.time()) # default date info
176
"""Run it and collect output.
179
1 (true) If everything went well.
180
0 (false) If there were problems.
182
if not self.executable:
183
logging.error('Could not locate "%s"' % self.long_name)
186
finfo = os.stat(self.executable)
187
self.date = time.localtime(finfo[stat.ST_MTIME])
189
logging.info('Running: %s %s </dev/null 2>&1'
190
% (self.executable, FLAGS.help_flag))
191
# --help output is often routed to stderr, so we combine with stdout.
192
# Re-direct stdin to /dev/null to encourage programs that
193
# don't understand --help to exit.
194
(child_stdin, child_stdout_and_stderr) = os.popen4(
195
[self.executable, FLAGS.help_flag])
196
child_stdin.close() # '</dev/null'
197
self.output = child_stdout_and_stderr.readlines()
198
child_stdout_and_stderr.close()
199
if len(self.output) < _MIN_VALID_USAGE_MSG:
200
logging.error('Error: "%s %s" returned only %d lines: %s'
201
% (self.name, FLAGS.help_flag,
202
len(self.output), self.output))
207
"""Parse program output."""
208
(start_line, lang) = self.ParseDesc()
212
self.ParsePythonFlags(start_line)
214
self.ParseCFlags(start_line)
216
self.ParseJavaFlags(start_line)
218
def ParseDesc(self, start_line=0):
219
"""Parse the initial description.
221
This could be Python or C++.
224
(start_line, lang_type)
225
start_line Line to start parsing flags on (int)
226
lang_type Either 'python' or 'c'
227
(-1, '') if the flags start could not be found
229
exec_mod_start = self.executable + ':'
232
start_line = 0 # ignore the passed-in arg for now (?)
233
for start_line in range(start_line, len(self.output)): # collect top description
234
line = self.output[start_line].rstrip()
235
# Python flags start with 'flags:\n'
237
and len(self.output) > start_line+1
238
and '' == self.output[start_line+1].rstrip()):
240
logging.debug('Flags start (python): %s' % line)
241
return (start_line, 'python')
242
# SWIG flags just have the module name followed by colon.
243
if exec_mod_start == line:
244
logging.debug('Flags start (swig): %s' % line)
245
return (start_line, 'python')
246
# C++ flags begin after a blank line and with a constant string
247
if after_blank and line.startswith(' Flags from '):
248
logging.debug('Flags start (c): %s' % line)
249
return (start_line, 'c')
250
# java flags begin with a constant string
251
if line == 'where flags are':
252
logging.debug('Flags start (java): %s' % line)
253
start_line += 2 # skip "Standard flags:"
254
return (start_line, 'java')
256
logging.debug('Desc: %s' % line)
257
self.desc.append(line)
258
after_blank = (line == '')
260
logging.warn('Never found the start of the flags section for "%s"!'
264
def ParsePythonFlags(self, start_line=0):
265
"""Parse python/swig style flags."""
266
modname = None # name of current module
269
for line_num in range(start_line, len(self.output)): # collect flags
270
line = self.output[line_num].rstrip()
274
mobj = self.module_py_re.match(line)
275
if mobj: # start of a new module
276
modname = mobj.group(1)
277
logging.debug('Module: %s' % line)
280
self.module_list.append(modname)
281
self.modules.setdefault(modname, [])
282
modlist = self.modules[modname]
286
mobj = self.flag_py_re.match(line)
287
if mobj: # start of a new flag
290
logging.debug('Flag: %s' % line)
291
flag = Flag(mobj.group(1), mobj.group(2))
294
if not flag: # continuation of a flag
295
logging.error('Flag info, but no current flag "%s"' % line)
296
mobj = self.flag_default_py_re.match(line)
297
if mobj: # (default: '...')
298
flag.default = mobj.group(1)
299
logging.debug('Fdef: %s' % line)
301
mobj = self.flag_tips_py_re.match(line)
303
flag.tips = mobj.group(1)
304
logging.debug('Ftip: %s' % line)
306
if flag and flag.help:
307
flag.help += line # multiflags tack on an extra line
309
logging.info('Extra: %s' % line)
313
def ParseCFlags(self, start_line=0):
314
"""Parse C style flags."""
315
modname = None # name of current module
318
for line_num in range(start_line, len(self.output)): # collect flags
319
line = self.output[line_num].rstrip()
320
if not line: # blank lines terminate flags
321
if flag: # save last flag
326
mobj = self.module_c_re.match(line)
327
if mobj: # start of a new module
328
modname = mobj.group(1)
329
logging.debug('Module: %s' % line)
332
self.module_list.append(modname)
333
self.modules.setdefault(modname, [])
334
modlist = self.modules[modname]
338
mobj = self.flag_c_re.match(line)
339
if mobj: # start of a new flag
340
if flag: # save last flag
342
logging.debug('Flag: %s' % line)
343
flag = Flag(mobj.group(1), mobj.group(2))
346
# append to flag help. type and default are part of the main text
348
flag.help += ' ' + line.strip()
350
logging.info('Extra: %s' % line)
354
def ParseJavaFlags(self, start_line=0):
355
"""Parse Java style flags (com.google.common.flags)."""
356
# The java flags prints starts with a "Standard flags" "module"
357
# that doesn't follow the standard module syntax.
358
modname = 'Standard flags' # name of current module
359
self.module_list.append(modname)
360
self.modules.setdefault(modname, [])
361
modlist = self.modules[modname]
364
for line_num in range(start_line, len(self.output)): # collect flags
365
line = self.output[line_num].rstrip()
366
logging.vlog(2, 'Line: "%s"' % line)
367
if not line: # blank lines terminate module
368
if flag: # save last flag
373
mobj = self.module_java_re.match(line)
374
if mobj: # start of a new module
375
modname = mobj.group(1)
376
logging.debug('Module: %s' % line)
379
self.module_list.append(modname)
380
self.modules.setdefault(modname, [])
381
modlist = self.modules[modname]
385
mobj = self.flag_java_re.match(line)
386
if mobj: # start of a new flag
387
if flag: # save last flag
389
logging.debug('Flag: %s' % line)
390
flag = Flag(mobj.group(1), mobj.group(2))
393
# append to flag help. type and default are part of the main text
395
flag.help += ' ' + line.strip()
397
logging.info('Extra: %s' % line)
402
"""Filter parsed data to create derived fields."""
407
for i in range(len(self.desc)): # replace full path with name
408
if self.desc[i].find(self.executable) >= 0:
409
self.desc[i] = self.desc[i].replace(self.executable, self.name)
411
self.short_desc = self.desc[0]
412
word_list = self.short_desc.split(' ')
413
all_names = [ self.name, self.short_name, ]
414
# Since the short_desc is always listed right after the name,
415
# trim it from the short_desc
416
while word_list and (word_list[0] in all_names
417
or word_list[0].lower() in all_names):
419
self.short_desc = '' # signal need to reconstruct
420
if not self.short_desc and word_list:
421
self.short_desc = ' '.join(word_list)
424
class GenerateDoc(object):
425
"""Base class to output flags information."""
427
def __init__(self, proginfo, directory='.'):
428
"""Create base object.
430
proginfo A ProgramInfo object
431
directory Directory to write output into
434
self.dirname = directory
437
"""Output all sections of the page."""
443
def Open(self): raise NotImplementedError # define in subclass
444
def Header(self): raise NotImplementedError # define in subclass
445
def Body(self): raise NotImplementedError # define in subclass
446
def Footer(self): raise NotImplementedError # define in subclass
449
class GenerateMan(GenerateDoc):
450
"""Output a man page."""
452
def __init__(self, proginfo, directory='.'):
453
"""Create base object.
455
proginfo A ProgramInfo object
456
directory Directory to write output into
458
GenerateDoc.__init__(self, proginfo, directory)
461
if self.dirname == '-':
462
logging.info('Writing to stdout')
465
self.file_path = '%s.1' % os.path.join(self.dirname, self.info.name)
466
logging.info('Writing: %s' % self.file_path)
467
self.fp = open(self.file_path, 'w')
471
'.\\" DO NOT MODIFY THIS FILE! It was generated by gflags2man %s\n'
474
'.TH %s "1" "%s" "%s" "User Commands"\n'
475
% (self.info.name, time.strftime('%x', self.info.date), self.info.name))
477
'.SH NAME\n%s \\- %s\n' % (self.info.name, self.info.short_desc))
479
'.SH SYNOPSIS\n.B %s\n[\\fIFLAGS\\fR]...\n' % self.info.name)
483
'.SH DESCRIPTION\n.\\" Add any additional description here\n.PP\n')
484
for ln in self.info.desc:
485
self.fp.write('%s\n' % ln)
488
# This shows flags in the original order
489
for modname in self.info.module_list:
490
if modname.find(self.info.executable) >= 0:
491
mod = modname.replace(self.info.executable, self.info.name)
494
self.fp.write('\n.P\n.I %s\n' % mod)
495
for flag in self.info.modules[modname]:
496
help_string = flag.help
497
if flag.default or flag.tips:
498
help_string += '\n.br\n'
500
help_string += ' (default: \'%s\')' % flag.default
502
help_string += ' (%s)' % flag.tips
504
'.TP\n%s\n%s\n' % (flag.desc, help_string))
508
'.SH COPYRIGHT\nCopyright \(co %s Google.\n'
509
% time.strftime('%Y', self.info.date))
510
self.fp.write('Gflags2man created this page from "%s %s" output.\n'
511
% (self.info.name, FLAGS.help_flag))
512
self.fp.write('\nGflags2man was written by Dan Christian. '
513
' Note that the date on this'
514
' page is the modification date of %s.\n' % self.info.name)
518
argv = FLAGS(argv) # handles help as well
520
print >>sys.stderr, __doc__
521
print >>sys.stderr, "flags:"
522
print >>sys.stderr, str(FLAGS)
526
prog = ProgramInfo(arg)
531
doc = GenerateMan(prog, FLAGS.dest_dir)
535
if __name__ == '__main__':