3 # Copyright (c) 2006, 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.
33 """gflags2man runs a Google flags base program and generates a man page.
35 Run the program, parse the output, and then format that into a man
39 gflags2man <program> [program] ...
42 # TODO(csilvers): work with windows paths (\) as well as unix (/)
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.
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
):
68 return os
.environ
.get('TMPDIR', '/tmp')
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')
79 _MIN_VALID_USAGE_MSG
= 9 # if fewer lines than this, help is suspect
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
92 def usage(self
, shorthelp
=0):
93 print >>sys
.stderr
, __doc__
94 print >>sys
.stderr
, "flags:"
95 print >>sys
.stderr
, str(FLAGS
)
101 def GetRealPath(filename
):
102 """Given an executable filename, find in the PATH or find absolute path.
104 filename An executable filename (string)
106 Absolute version of filename.
107 None if filename could not be found locally, absolutely, or in PATH
109 if os
.path
.isabs(filename
): # already absolute
112 if filename
.startswith('./') or filename
.startswith('../'): # relative
113 return os
.path
.abspath(filename
)
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
)
122 if os
.path
.exists(filename
):
123 return os
.path
.abspath(filename
)
124 return None # could not determine
127 """The information about a single flag."""
129 def __init__(self
, flag_desc
, help):
130 """Create the flag object.
132 flag_desc The command line forms this could take. (string)
133 help The help text (string)
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
141 class ProgramInfo(object):
142 """All the information gleaned from running a program with --help."""
144 # Match a module block start, for python scripts --help
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+(.*)$')
151 flag_default_py_re
= re
.compile(r
'\s+\(default:\s+\'(.*)\'\
)$
')
153 flag_tips_py_re = re.compile(r'\s
+\
((.*)\
)$
')
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
+(.*)$
')
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
+(.*)$
')
169 def __init__(self, executable):
170 """Create object with executable.
172 executable Program to execute (string)
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
186 """Run it and collect output.
189 1 (true) If everything went well.
190 0 (false) If there were problems.
192 if not self.executable:
193 logging.error('Could
not locate
"%s"' % self.long_name)
196 finfo = os.stat(self.executable)
197 self.date = time.localtime(finfo[stat.ST_MTIME])
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
))
217 """Parse program output."""
218 (start_line
, lang
) = self
.ParseDesc()
222 self
.ParsePythonFlags(start_line
)
224 self
.ParseCFlags(start_line
)
226 self
.ParseJavaFlags(start_line
)
228 def ParseDesc(self
, start_line
=0):
229 """Parse the initial description.
231 This could be Python or C++.
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
239 exec_mod_start
= self
.executable
+ ':'
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'
247 and len(self
.output
) > start_line
+1
248 and '' == self
.output
[start_line
+1].rstrip()):
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')
266 logging
.debug('Desc: %s' % line
)
267 self
.desc
.append(line
)
268 after_blank
= (line
== '')
270 logging
.warn('Never found the start of the flags section for "%s"!'
274 def ParsePythonFlags(self
, start_line
=0):
275 """Parse python/swig style flags."""
276 modname
= None # name of current module
279 for line_num
in range(start_line
, len(self
.output
)): # collect flags
280 line
= self
.output
[line_num
].rstrip()
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
)
290 self
.module_list
.append(modname
)
291 self
.modules
.setdefault(modname
, [])
292 modlist
= self
.modules
[modname
]
296 mobj
= self
.flag_py_re
.match(line
)
297 if mobj
: # start of a new flag
300 logging
.debug('Flag: %s' % line
)
301 flag
= Flag(mobj
.group(1), mobj
.group(2))
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
)
311 mobj
= self
.flag_tips_py_re
.match(line
)
313 flag
.tips
= mobj
.group(1)
314 logging
.debug('Ftip: %s' % line
)
316 if flag
and flag
.help:
317 flag
.help += line
# multiflags tack on an extra line
319 logging
.info('Extra: %s' % line
)
323 def ParseCFlags(self
, start_line
=0):
324 """Parse C style flags."""
325 modname
= None # name of current module
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
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
)
342 self
.module_list
.append(modname
)
343 self
.modules
.setdefault(modname
, [])
344 modlist
= self
.modules
[modname
]
348 mobj
= self
.flag_c_re
.match(line
)
349 if mobj
: # start of a new flag
350 if flag
: # save last flag
352 logging
.debug('Flag: %s' % line
)
353 flag
= Flag(mobj
.group(1), mobj
.group(2))
356 # append to flag help. type and default are part of the main text
358 flag
.help += ' ' + line
.strip()
360 logging
.info('Extra: %s' % line
)
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
]
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
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
)
389 self
.module_list
.append(modname
)
390 self
.modules
.setdefault(modname
, [])
391 modlist
= self
.modules
[modname
]
395 mobj
= self
.flag_java_re
.match(line
)
396 if mobj
: # start of a new flag
397 if flag
: # save last flag
399 logging
.debug('Flag: %s' % line
)
400 flag
= Flag(mobj
.group(1), mobj
.group(2))
403 # append to flag help. type and default are part of the main text
405 flag
.help += ' ' + line
.strip()
407 logging
.info('Extra: %s' % line
)
412 """Filter parsed data to create derived fields."""
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
)
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
):
429 self
.short_desc
= '' # signal need to reconstruct
430 if not self
.short_desc
and word_list
:
431 self
.short_desc
= ' '.join(word_list
)
434 class GenerateDoc(object):
435 """Base class to output flags information."""
437 def __init__(self
, proginfo
, directory
='.'):
438 """Create base object.
440 proginfo A ProgramInfo object
441 directory Directory to write output into
444 self
.dirname
= directory
447 """Output all sections of the page."""
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
459 class GenerateMan(GenerateDoc
):
460 """Output a man page."""
462 def __init__(self
, proginfo
, directory
='.'):
463 """Create base object.
465 proginfo A ProgramInfo object
466 directory Directory to write output into
468 GenerateDoc
.__init
__(self
, proginfo
, directory
)
471 if self
.dirname
== '-':
472 logging
.info('Writing to stdout')
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')
481 '.\\" DO NOT MODIFY THIS FILE! It was generated by gflags2man %s\n'
484 '.TH %s "1" "%s" "%s" "User Commands"\n'
485 % (self
.info
.name
, time
.strftime('%x', self
.info
.date
), self
.info
.name
))
487 '.SH NAME\n%s \\- %s\n' % (self
.info
.name
, self
.info
.short_desc
))
489 '.SH SYNOPSIS\n.B %s\n[\\fIFLAGS\\fR]...\n' % self
.info
.name
)
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
)
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
)
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'
510 help_string
+= ' (default: \'%s\')' % flag
.default
512 help_string
+= ' (%s)' % flag
.tips
514 '.TP\n%s\n%s\n' % (flag
.desc
, help_string
))
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
)
528 argv
= FLAGS(argv
) # handles help as well
530 app
.usage(shorthelp
=1)
534 prog
= ProgramInfo(arg
)
539 doc
= GenerateMan(prog
, FLAGS
.dest_dir
)
543 if __name__
== '__main__':