• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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