Roll src/third_party/WebKit 8139f33:52503da (svn 201975:201976)
[chromium-blink-merge.git] / third_party / python_gflags / gflags2man.py
blob3a50f9e19fcf0fb6e78ba24daf868bd2068c8bbb
1 #!/usr/bin/env python
3 # Copyright (c) 2006, Google Inc.
4 # All rights reserved.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are
8 # met:
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.
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
36 page.
38 Usage:
39 gflags2man <program> [program] ...
40 """
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.
51 import os
52 import re
53 import sys
54 import stat
55 import time
57 import gflags
59 _VERSION = '0.1'
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')
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')
79 _MIN_VALID_USAGE_MSG = 9 # if fewer lines than this, help is suspect
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()
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
109 if os.path.isabs(filename): # already absolute
110 return filename
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)
121 return tryname
122 if os.path.exists(filename):
123 return os.path.abspath(filename)
124 return None # could not determine
126 class Flag(object):
127 """The information about a single flag."""
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)
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
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+\((.*)\)$')
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.
171 Args:
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
185 def Run(self):
186 """Run it and collect output.
188 Returns:
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)
194 return 0
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))
213 return 0
214 return 1
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)
228 def ParseDesc(self, start_line=0):
229 """Parse the initial description.
231 This could be Python or C++.
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
239 exec_mod_start = self.executable + ':'
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')
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, '')
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
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
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
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)
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
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
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
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)
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
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
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
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
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)
411 def Filter(self):
412 """Filter parsed data to create derived fields."""
413 if not self.desc:
414 self.short_desc = ''
415 return
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):
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)
434 class GenerateDoc(object):
435 """Base class to output flags information."""
437 def __init__(self, proginfo, directory='.'):
438 """Create base object.
439 Args:
440 proginfo A ProgramInfo object
441 directory Directory to write output into
443 self.info = proginfo
444 self.dirname = directory
446 def Output(self):
447 """Output all sections of the page."""
448 self.Open()
449 self.Header()
450 self.Body()
451 self.Footer()
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.
464 Args:
465 proginfo A ProgramInfo object
466 directory Directory to write output into
468 GenerateDoc.__init__(self, proginfo, directory)
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')
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)
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))
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)
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
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
543 if __name__ == '__main__':
544 app.run()