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 35 Run the program, parse the output, and then format that into a man 36 page. 37 38 Usage: 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 51 import os 52 import re 53 import sys 54 import stat 55 import time 56 57 import gflags 58 59 _VERSION = '0.1' 60 61 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') 69 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') 77 78 79 _MIN_VALID_USAGE_MSG = 9 # if fewer lines than this, help is suspect 80 81 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() 99 100 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 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 126 class 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 141 class 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 434 class 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 459 class 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 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 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 543 if __name__ == '__main__': 544 app.run() 545