OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # |
| 3 # Copyright (c) 2007, 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 """gflags2man runs a Google flags base program and generates a man page. |
| 33 |
| 34 Run the program, parse the output, and then format that into a man |
| 35 page. |
| 36 |
| 37 Usage: |
| 38 gflags2man <program> [program] ... |
| 39 """ |
| 40 |
| 41 # TODO(csilvers): work with windows paths (\) as well as unix (/) |
| 42 |
| 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. |
| 47 |
| 48 __author__ = 'Dan Christian' |
| 49 |
| 50 import os |
| 51 import re |
| 52 import sys |
| 53 import stat |
| 54 import time |
| 55 |
| 56 import gflags |
| 57 |
| 58 _VERSION = '0.1' |
| 59 |
| 60 |
| 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): |
| 65 return homeman |
| 66 else: |
| 67 return os.environ.get('TMPDIR', '/tmp') |
| 68 |
| 69 FLAGS = gflags.FLAGS |
| 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') |
| 76 |
| 77 _MIN_VALID_USAGE_MSG = 9 # if fewer lines than this, help is suspect |
| 78 |
| 79 |
| 80 class Logging: |
| 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 |
| 88 logging = Logging() |
| 89 |
| 90 |
| 91 def GetRealPath(filename): |
| 92 """Given an executable filename, find in the PATH or find absolute path. |
| 93 Args: |
| 94 filename An executable filename (string) |
| 95 Returns: |
| 96 Absolute version of filename. |
| 97 None if filename could not be found locally, absolutely, or in PATH |
| 98 """ |
| 99 if os.path.isabs(filename): # already absolute |
| 100 return filename |
| 101 |
| 102 if filename.startswith('./') or filename.startswith('../'): # relative |
| 103 return os.path.abspath(filename) |
| 104 |
| 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) |
| 111 return tryname |
| 112 if os.path.exists(filename): |
| 113 return os.path.abspath(filename) |
| 114 return None # could not determine |
| 115 |
| 116 class Flag(object): |
| 117 """The information about a single flag.""" |
| 118 |
| 119 def __init__(self, flag_desc, help): |
| 120 """Create the flag object. |
| 121 Args: |
| 122 flag_desc The command line forms this could take. (string) |
| 123 help The help text (string) |
| 124 """ |
| 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 |
| 129 |
| 130 |
| 131 class ProgramInfo(object): |
| 132 """All the information gleaned from running a program with --help.""" |
| 133 |
| 134 # Match a module block start, for python scripts --help |
| 135 # "goopy.logging:" |
| 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+(.*)$') |
| 140 # " (default: '0')" |
| 141 flag_default_py_re = re.compile(r'\s+\(default:\s+\'(.*)\'\)$') |
| 142 # " (an integer)" |
| 143 flag_tips_py_re = re.compile(r'\s+\((.*)\)$') |
| 144 |
| 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+(.*)$') |
| 151 |
| 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+(.*)$') |
| 158 |
| 159 def __init__(self, executable): |
| 160 """Create object with executable. |
| 161 Args: |
| 162 executable Program to execute (string) |
| 163 """ |
| 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 |
| 174 |
| 175 def Run(self): |
| 176 """Run it and collect output. |
| 177 |
| 178 Returns: |
| 179 1 (true) If everything went well. |
| 180 0 (false) If there were problems. |
| 181 """ |
| 182 if not self.executable: |
| 183 logging.error('Could not locate "%s"' % self.long_name) |
| 184 return 0 |
| 185 |
| 186 finfo = os.stat(self.executable) |
| 187 self.date = time.localtime(finfo[stat.ST_MTIME]) |
| 188 |
| 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)) |
| 203 return 0 |
| 204 return 1 |
| 205 |
| 206 def Parse(self): |
| 207 """Parse program output.""" |
| 208 (start_line, lang) = self.ParseDesc() |
| 209 if start_line < 0: |
| 210 return |
| 211 if 'python' == lang: |
| 212 self.ParsePythonFlags(start_line) |
| 213 elif 'c' == lang: |
| 214 self.ParseCFlags(start_line) |
| 215 elif 'java' == lang: |
| 216 self.ParseJavaFlags(start_line) |
| 217 |
| 218 def ParseDesc(self, start_line=0): |
| 219 """Parse the initial description. |
| 220 |
| 221 This could be Python or C++. |
| 222 |
| 223 Returns: |
| 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 |
| 228 """ |
| 229 exec_mod_start = self.executable + ':' |
| 230 |
| 231 after_blank = 0 |
| 232 start_line = 0 # ignore the passed-in arg for now (?) |
| 233 for start_line in range(start_line, len(self.output)): # collect top descrip
tion |
| 234 line = self.output[start_line].rstrip() |
| 235 # Python flags start with 'flags:\n' |
| 236 if ('flags:' == line |
| 237 and len(self.output) > start_line+1 |
| 238 and '' == self.output[start_line+1].rstrip()): |
| 239 start_line += 2 |
| 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') |
| 255 |
| 256 logging.debug('Desc: %s' % line) |
| 257 self.desc.append(line) |
| 258 after_blank = (line == '') |
| 259 else: |
| 260 logging.warn('Never found the start of the flags section for "%s"!' |
| 261 % self.long_name) |
| 262 return (-1, '') |
| 263 |
| 264 def ParsePythonFlags(self, start_line=0): |
| 265 """Parse python/swig style flags.""" |
| 266 modname = None # name of current module |
| 267 modlist = [] |
| 268 flag = None |
| 269 for line_num in range(start_line, len(self.output)): # collect flags |
| 270 line = self.output[line_num].rstrip() |
| 271 if not line: # blank |
| 272 continue |
| 273 |
| 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) |
| 278 if flag: |
| 279 modlist.append(flag) |
| 280 self.module_list.append(modname) |
| 281 self.modules.setdefault(modname, []) |
| 282 modlist = self.modules[modname] |
| 283 flag = None |
| 284 continue |
| 285 |
| 286 mobj = self.flag_py_re.match(line) |
| 287 if mobj: # start of a new flag |
| 288 if flag: |
| 289 modlist.append(flag) |
| 290 logging.debug('Flag: %s' % line) |
| 291 flag = Flag(mobj.group(1), mobj.group(2)) |
| 292 continue |
| 293 |
| 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) |
| 300 continue |
| 301 mobj = self.flag_tips_py_re.match(line) |
| 302 if mobj: # (tips) |
| 303 flag.tips = mobj.group(1) |
| 304 logging.debug('Ftip: %s' % line) |
| 305 continue |
| 306 if flag and flag.help: |
| 307 flag.help += line # multiflags tack on an extra line |
| 308 else: |
| 309 logging.info('Extra: %s' % line) |
| 310 if flag: |
| 311 modlist.append(flag) |
| 312 |
| 313 def ParseCFlags(self, start_line=0): |
| 314 """Parse C style flags.""" |
| 315 modname = None # name of current module |
| 316 modlist = [] |
| 317 flag = None |
| 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 |
| 322 modlist.append(flag) |
| 323 flag = None |
| 324 continue |
| 325 |
| 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) |
| 330 if flag: |
| 331 modlist.append(flag) |
| 332 self.module_list.append(modname) |
| 333 self.modules.setdefault(modname, []) |
| 334 modlist = self.modules[modname] |
| 335 flag = None |
| 336 continue |
| 337 |
| 338 mobj = self.flag_c_re.match(line) |
| 339 if mobj: # start of a new flag |
| 340 if flag: # save last flag |
| 341 modlist.append(flag) |
| 342 logging.debug('Flag: %s' % line) |
| 343 flag = Flag(mobj.group(1), mobj.group(2)) |
| 344 continue |
| 345 |
| 346 # append to flag help. type and default are part of the main text |
| 347 if flag: |
| 348 flag.help += ' ' + line.strip() |
| 349 else: |
| 350 logging.info('Extra: %s' % line) |
| 351 if flag: |
| 352 modlist.append(flag) |
| 353 |
| 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] |
| 362 flag = None |
| 363 |
| 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 |
| 369 modlist.append(flag) |
| 370 flag = None |
| 371 continue |
| 372 |
| 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) |
| 377 if flag: |
| 378 modlist.append(flag) |
| 379 self.module_list.append(modname) |
| 380 self.modules.setdefault(modname, []) |
| 381 modlist = self.modules[modname] |
| 382 flag = None |
| 383 continue |
| 384 |
| 385 mobj = self.flag_java_re.match(line) |
| 386 if mobj: # start of a new flag |
| 387 if flag: # save last flag |
| 388 modlist.append(flag) |
| 389 logging.debug('Flag: %s' % line) |
| 390 flag = Flag(mobj.group(1), mobj.group(2)) |
| 391 continue |
| 392 |
| 393 # append to flag help. type and default are part of the main text |
| 394 if flag: |
| 395 flag.help += ' ' + line.strip() |
| 396 else: |
| 397 logging.info('Extra: %s' % line) |
| 398 if flag: |
| 399 modlist.append(flag) |
| 400 |
| 401 def Filter(self): |
| 402 """Filter parsed data to create derived fields.""" |
| 403 if not self.desc: |
| 404 self.short_desc = '' |
| 405 return |
| 406 |
| 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) |
| 410 |
| 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): |
| 418 del word_list[0] |
| 419 self.short_desc = '' # signal need to reconstruct |
| 420 if not self.short_desc and word_list: |
| 421 self.short_desc = ' '.join(word_list) |
| 422 |
| 423 |
| 424 class GenerateDoc(object): |
| 425 """Base class to output flags information.""" |
| 426 |
| 427 def __init__(self, proginfo, directory='.'): |
| 428 """Create base object. |
| 429 Args: |
| 430 proginfo A ProgramInfo object |
| 431 directory Directory to write output into |
| 432 """ |
| 433 self.info = proginfo |
| 434 self.dirname = directory |
| 435 |
| 436 def Output(self): |
| 437 """Output all sections of the page.""" |
| 438 self.Open() |
| 439 self.Header() |
| 440 self.Body() |
| 441 self.Footer() |
| 442 |
| 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 |
| 447 |
| 448 |
| 449 class GenerateMan(GenerateDoc): |
| 450 """Output a man page.""" |
| 451 |
| 452 def __init__(self, proginfo, directory='.'): |
| 453 """Create base object. |
| 454 Args: |
| 455 proginfo A ProgramInfo object |
| 456 directory Directory to write output into |
| 457 """ |
| 458 GenerateDoc.__init__(self, proginfo, directory) |
| 459 |
| 460 def Open(self): |
| 461 if self.dirname == '-': |
| 462 logging.info('Writing to stdout') |
| 463 self.fp = sys.stdout |
| 464 else: |
| 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') |
| 468 |
| 469 def Header(self): |
| 470 self.fp.write( |
| 471 '.\\" DO NOT MODIFY THIS FILE! It was generated by gflags2man %s\n' |
| 472 % _VERSION) |
| 473 self.fp.write( |
| 474 '.TH %s "1" "%s" "%s" "User Commands"\n' |
| 475 % (self.info.name, time.strftime('%x', self.info.date), self.info.name)) |
| 476 self.fp.write( |
| 477 '.SH NAME\n%s \\- %s\n' % (self.info.name, self.info.short_desc)) |
| 478 self.fp.write( |
| 479 '.SH SYNOPSIS\n.B %s\n[\\fIFLAGS\\fR]...\n' % self.info.name) |
| 480 |
| 481 def Body(self): |
| 482 self.fp.write( |
| 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) |
| 486 self.fp.write( |
| 487 '.SH OPTIONS\n') |
| 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) |
| 492 else: |
| 493 mod = modname |
| 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' |
| 499 if flag.default: |
| 500 help_string += ' (default: \'%s\')' % flag.default |
| 501 if flag.tips: |
| 502 help_string += ' (%s)' % flag.tips |
| 503 self.fp.write( |
| 504 '.TP\n%s\n%s\n' % (flag.desc, help_string)) |
| 505 |
| 506 def Footer(self): |
| 507 self.fp.write( |
| 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) |
| 515 |
| 516 |
| 517 def main(argv): |
| 518 argv = FLAGS(argv) # handles help as well |
| 519 if len(argv) <= 1: |
| 520 print >>sys.stderr, __doc__ |
| 521 print >>sys.stderr, "flags:" |
| 522 print >>sys.stderr, str(FLAGS) |
| 523 return 1 |
| 524 |
| 525 for arg in argv[1:]: |
| 526 prog = ProgramInfo(arg) |
| 527 if not prog.Run(): |
| 528 continue |
| 529 prog.Parse() |
| 530 prog.Filter() |
| 531 doc = GenerateMan(prog, FLAGS.dest_dir) |
| 532 doc.Output() |
| 533 return 0 |
| 534 |
| 535 if __name__ == '__main__': |
| 536 main(sys.argv) |
OLD | NEW |