diff options
Diffstat (limited to 'help2rst.py')
-rwxr-xr-x | help2rst.py | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/help2rst.py b/help2rst.py new file mode 100755 index 0000000..245f669 --- /dev/null +++ b/help2rst.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# script to produce rst file from program's help output. + +import sys +import re +import argparse + +arg_indent = ' ' * 14 + +def help2man(infile): + # We assume that first line is usage line like this: + # + # Usage: nghttp [OPTIONS]... URI... + # + # The second line is description of the command. Multiple lines + # are permitted. The blank line signals the end of this section. + # After that, we parses positional and optional arguments. + # + # The positional argument is enclosed with < and >: + # + # <PRIVATE_KEY> + # + # We may describe default behavior without any options by encoding + # ( and ): + # + # (default mode) + # + # "Options:" is treated specially and produces "OPTIONS" section. + # We allow subsection under OPTIONS. Lines not starting with (, < + # and Options: are treated as subsection name and produces section + # one level down: + # + # TLS/SSL: + # + # The above is an example of subsection. + # + # The description of arguments must be indented by len(arg_indent) + # characters. The default value should be placed in separate line + # and should be start with "Default: " after indentation. + + line = infile.readline().strip() + m = re.match(r'^Usage: (.*)', line) + if not m: + print('usage line is invalid. Expected following lines:') + print('Usage: cmdname ...') + sys.exit(1) + synopsis = m.group(1).split(' ', 1) + if len(synopsis) == 2: + cmdname, args = synopsis + else: + cmdname, args = synopsis[0], '' + + description = [] + for line in infile: + line = line.strip() + if not line: + break + description.append(line) + + print(''' +.. GENERATED by help2rst.py. DO NOT EDIT DIRECTLY. + +.. program:: {cmdname} + +{cmdname}(1) +{cmdnameunderline} + +SYNOPSIS +-------- + +**{cmdname}** {args} + +DESCRIPTION +----------- + +{description} +'''.format(cmdname=cmdname, args=args, + cmdnameunderline='=' * (len(cmdname) + 3), + synopsis=synopsis, description=format_text('\n'.join(description)))) + + in_arg = False + in_footer = False + + for line in infile: + line = line.rstrip() + + if not line.strip() and in_arg: + print() + continue + if line.startswith(' ') and in_arg: + if not line.startswith(arg_indent): + sys.stderr.write('warning: argument description is not indented correctly. We need {} spaces as indentation.\n'.format(len(arg_indent))) + print('{}'.format(format_arg_text(line[len(arg_indent):]))) + continue + + if in_arg: + print() + in_arg = False + + if line == '--': + in_footer = True + continue + + if in_footer: + print(line.strip()) + continue + + if line == 'Options:': + print('OPTIONS') + print('-------') + print() + continue + + if line.startswith(' <'): + # positional argument + m = re.match(r'^(?:\s+)([a-zA-Z0-9-_<>]+)(.*)', line) + argname, rest = m.group(1), m.group(2) + print('.. describe:: {}'.format(argname)) + print() + print('{}'.format(format_arg_text(rest.strip()))) + in_arg = True + continue + + if line.startswith(' ('): + # positional argument + m = re.match(r'^(?:\s+)(\([a-zA-Z0-9-_<> ]+\))(.*)', line) + argname, rest = m.group(1), m.group(2) + print('.. describe:: {}'.format(argname)) + print() + print('{}'.format(format_arg_text(rest.strip()))) + in_arg = True + continue + + if line.startswith(' -'): + # optional argument + m = re.match( + r'^(?:\s+)(-\S+?(?:, -\S+?)*)($| .*)', + line) + argname, rest = m.group(1), m.group(2) + print('.. option:: {}'.format(argname)) + print() + rest = rest.strip() + if len(rest): + print('{}'.format(format_arg_text(rest))) + in_arg = True + continue + + if not line.startswith(' ') and line.endswith(':'): + # subsection + subsec = line.strip()[:-1] + print('{}'.format(subsec)) + print('{}'.format('~' * len(subsec))) + print() + continue + + print(line.strip()) + +def format_text(text): + # escape *, but don't escape * if it is used as bullet list. + m = re.match(r'^\s*\*\s+', text) + if m: + text = text[:len(m.group(0))] + re.sub(r'\*', r'\*', text[len(m.group(0)):]) + else: + text = re.sub(r'\*', r'\*', text) + # markup option reference + text = re.sub(r'(^|\s)(-[a-zA-Z]|--[a-zA-Z0-9-]+)', + r'\1:option:`\2`', text) + # sphinx does not like markup like ':option:`-f`='. We need + # backslash between ` and =. + text = re.sub(r'(:option:`.*?`)(\S)', r'\1\\\2', text) + # file path should be italic + text = re.sub(r'(^|\s|\'|")(/[^\s\'"]*)', r'\1*\2*', text) + return text + +def format_arg_text(text): + if text.strip().startswith('Default: '): + return '\n ' + re.sub(r'^(\s*Default: )(.*)$', r'\1``\2``', text) + return ' {}'.format(format_text(text)) + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Produces rst document from help output.') + parser.add_argument('-i', '--include', metavar='FILE', + help='include content of <FILE> as verbatim. It should be ReST formatted text.') + args = parser.parse_args() + help2man(sys.stdin) + if args.include: + print() + with open(args.include) as f: + sys.stdout.write(f.read()) |