# Copyright (C) 2011 Lars Wirzenius # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import optparse import re class ManpageGenerator(object): '''Fill in a manual page template from an OptionParser instance.''' def __init__(self, template, parser, arg_synopsis, cmd_synopsis): self.template = template self.parser = parser self.arg_synopsis = arg_synopsis self.cmd_synopsis = cmd_synopsis def sort_options(self, options): # Return the options in an option group in sorted order. This # is slightly tricky. Humans have such weird opinions of what # it means to be sorted. The main thing we care about is that # given a boolean setting foo, the options --foo and --no-foo # get sorted after each other. We do this by using the # option.from_setting attribute, which might not exist. The # list already includes the --no-foo options, which seems a # bit silly, but there you go. # # Split list of options into two: one with --foo, one with # --no-foo. Sort the first list. Insert options from second # list into first list. def is_neg_option(o): return (hasattr(o, 'from_setting') and o._long_opts and o._long_opts[0].startswith('--no-')) def split(options, setting): before = [] from_same = [] after = [] for o in options: s = getattr(o, 'from_setting', None) if s is setting: from_same.append(o) elif from_same: after.append(o) else: before.append(o) return before, from_same, after neg_options = [o for o in options if is_neg_option(o)] main_options = [o for o in options if o not in neg_options] main_options.sort(key=lambda o: (o._long_opts + o._short_opts)[0]) for neg in neg_options: before, from_same, after = split(main_options, neg.from_setting) main_options = before + from_same + [neg] + after return main_options def option_list(self, container): return self.sort_options(container.option_list) def format_template(self): sections = (('SYNOPSIS', self.format_synopsis()), ('OPTIONS', self.format_options())) text = self.template for section, contents in sections: pattern = '\n.SH %s\n' % section text = text.replace(pattern, pattern + contents) return text def format_synopsis(self): lines = [] lines += ['.nh'] lines += ['.B %s' % self.esc_dashes(self.parser.prog)] all_options = self.option_list(self.parser) for group in self.parser.option_groups: all_options += self.option_list(group) for option in self.sort_options(all_options): for spec in self.format_option_for_synopsis(option): lines += ['.RB [ %s ]' % spec] if self.cmd_synopsis: lines += ['.PP'] for cmd in sorted(self.cmd_synopsis): lines += ['.br', '.B %s' % self.esc_dashes(self.parser.prog), '.RI [ options ]', self.esc_dashes(cmd)] lines += self.format_argspec(self.cmd_synopsis[cmd]) elif self.arg_synopsis: lines += self.format_argspec(self.arg_synopsis) lines += ['.hy'] return ''.join('%s\n' % line for line in lines) def format_option_for_synopsis(self, option): if option.metavar: short_suffix = '\\fI%s' % self.esc_dashes(option.metavar) long_suffix = '\\fR=\\fI%s' % self.esc_dashes(option.metavar) else: short_suffix = '' long_suffix = '' for name in option._short_opts + option._long_opts: if name.startswith('--'): suffix = long_suffix else: suffix = short_suffix yield '%s%s' % (self.esc_dashes(name), suffix) def format_options(self): lines = [] for option in self.sort_options(self.parser.option_list): lines += self.format_option_for_options(option) for group in self.parser.option_groups: lines += ['.SS "%s"' % group.title] for option in self.sort_options(group.option_list): lines += self.format_option_for_options(option) return ''.join('%s\n' % line for line in lines) def format_option_for_options(self, option): lines = [] lines += ['.TP'] shorts = [self.esc_dashes(x) for x in option._short_opts] if option.metavar: longs = ['%s =\\fI%s' % (self.esc_dashes(x), option.metavar) for x in option._long_opts] else: longs = ['%s' % self.esc_dashes(x) for x in option._long_opts] lines += ['.BR ' + ' ", " '.join(shorts + longs)] lines += [self.esc_dots(self.expand_default(option).strip())] return lines def expand_default(self, option): default = self.parser.defaults.get(option.dest) if default is optparse.NO_DEFAULT or default is None: default = 'none' else: default = str(default) return option.help.replace('%default', default) def esc_dashes(self, optname): return '\\-'.join(optname.split('-')) def esc_dots(self, line): if line.startswith('.'): return '\\' + line else: return line def format_argspec(self, argspec): roman = re.compile(r'[^A-Z]+') italic = re.compile(r'[A-Z]+') words = ['.RI'] while argspec: m = roman.match(argspec) if m: words += [self.esc_dashes(m.group(0))] argspec = argspec[m.end():] else: words += ['""'] m = italic.match(argspec) if m: words += [self.esc_dashes(m.group(0))] argspec = argspec[m.end():] else: words += ['""'] return [' '.join(words)]