1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# script to produce rst file from program's help output. 5 6import sys 7import re 8import argparse 9 10arg_indent = ' ' * 14 11 12def help2man(infile): 13 # We assume that first line is usage line like this: 14 # 15 # Usage: nghttp [OPTIONS]... URI... 16 # 17 # The second line is description of the command. Multiple lines 18 # are permitted. The blank line signals the end of this section. 19 # After that, we parses positional and optional arguments. 20 # 21 # The positional argument is enclosed with < and >: 22 # 23 # <PRIVATE_KEY> 24 # 25 # We may describe default behavior without any options by encoding 26 # ( and ): 27 # 28 # (default mode) 29 # 30 # "Options:" is treated specially and produces "OPTIONS" section. 31 # We allow subsection under OPTIONS. Lines not starting with (, < 32 # and Options: are treated as subsection name and produces section 33 # one level down: 34 # 35 # TLS/SSL: 36 # 37 # The above is an example of subsection. 38 # 39 # The description of arguments must be indented by len(arg_indent) 40 # characters. The default value should be placed in separate line 41 # and should be start with "Default: " after indentation. 42 43 line = infile.readline().strip() 44 m = re.match(r'^Usage: (.*)', line) 45 if not m: 46 print('usage line is invalid. Expected following lines:') 47 print('Usage: cmdname ...') 48 sys.exit(1) 49 synopsis = m.group(1).split(' ', 1) 50 if len(synopsis) == 2: 51 cmdname, args = synopsis 52 else: 53 cmdname, args = synopsis[0], '' 54 55 description = [] 56 for line in infile: 57 line = line.strip() 58 if not line: 59 break 60 description.append(line) 61 62 print(''' 63.. GENERATED by help2rst.py. DO NOT EDIT DIRECTLY. 64 65.. program:: {cmdname} 66 67{cmdname}(1) 68{cmdnameunderline} 69 70SYNOPSIS 71-------- 72 73**{cmdname}** {args} 74 75DESCRIPTION 76----------- 77 78{description} 79'''.format(cmdname=cmdname, args=args, 80 cmdnameunderline='=' * (len(cmdname) + 3), 81 synopsis=synopsis, description=format_text('\n'.join(description)))) 82 83 in_arg = False 84 in_footer = False 85 86 for line in infile: 87 line = line.rstrip() 88 89 if not line.strip() and in_arg: 90 print() 91 continue 92 if line.startswith(' ') and in_arg: 93 if not line.startswith(arg_indent): 94 sys.stderr.write('warning: argument description is not indented correctly. We need {} spaces as indentation.\n'.format(len(arg_indent))) 95 print('{}'.format(format_arg_text(line[len(arg_indent):]))) 96 continue 97 98 if in_arg: 99 print() 100 in_arg = False 101 102 if line == '--': 103 in_footer = True 104 continue 105 106 if in_footer: 107 print(line.strip()) 108 continue 109 110 if line == 'Options:': 111 print('OPTIONS') 112 print('-------') 113 print() 114 continue 115 116 if line.startswith(' <'): 117 # positional argument 118 m = re.match(r'^(?:\s+)([a-zA-Z0-9-_<>]+)(.*)', line) 119 argname, rest = m.group(1), m.group(2) 120 print('.. describe:: {}'.format(argname)) 121 print() 122 print('{}'.format(format_arg_text(rest.strip()))) 123 in_arg = True 124 continue 125 126 if line.startswith(' ('): 127 # positional argument 128 m = re.match(r'^(?:\s+)(\([a-zA-Z0-9-_<> ]+\))(.*)', line) 129 argname, rest = m.group(1), m.group(2) 130 print('.. describe:: {}'.format(argname)) 131 print() 132 print('{}'.format(format_arg_text(rest.strip()))) 133 in_arg = True 134 continue 135 136 if line.startswith(' -'): 137 # optional argument 138 m = re.match( 139 r'^(?:\s+)(-\S+?(?:, -\S+?)*)($| .*)', 140 line) 141 argname, rest = m.group(1), m.group(2) 142 print('.. option:: {}'.format(argname)) 143 print() 144 rest = rest.strip() 145 if len(rest): 146 print('{}'.format(format_arg_text(rest))) 147 in_arg = True 148 continue 149 150 if not line.startswith(' ') and line.endswith(':'): 151 # subsection 152 subsec = line.strip()[:-1] 153 print('{}'.format(subsec)) 154 print('{}'.format('~' * len(subsec))) 155 print() 156 continue 157 158 print(line.strip()) 159 160def format_text(text): 161 # escape *, but don't escape * if it is used as bullet list. 162 m = re.match(r'^\s*\*\s+', text) 163 if m: 164 text = text[:len(m.group(0))] + re.sub(r'\*', r'\*', text[len(m.group(0)):]) 165 else: 166 text = re.sub(r'\*', r'\*', text) 167 # markup option reference 168 text = re.sub(r'(^|\s)(-[a-zA-Z]|--[a-zA-Z0-9-]+)', 169 r'\1:option:`\2`', text) 170 # sphinx does not like markup like ':option:`-f`='. We need 171 # backslash between ` and =. 172 text = re.sub(r'(:option:`.*?`)(\S)', r'\1\\\2', text) 173 # file path should be italic 174 text = re.sub(r'(^|\s|\'|")(/[^\s\'"]*)', r'\1*\2*', text) 175 return text 176 177def format_arg_text(text): 178 if text.strip().startswith('Default: '): 179 return '\n ' + re.sub(r'^(\s*Default: )(.*)$', r'\1``\2``', text) 180 return ' {}'.format(format_text(text)) 181 182if __name__ == '__main__': 183 parser = argparse.ArgumentParser( 184 description='Produces rst document from help output.') 185 parser.add_argument('-i', '--include', metavar='FILE', 186 help='include content of <FILE> as verbatim. It should be ReST formatted text.') 187 args = parser.parse_args() 188 help2man(sys.stdin) 189 if args.include: 190 print() 191 with open(args.include) as f: 192 sys.stdout.write(f.read()) 193