summaryrefslogtreecommitdiffstats
path: root/help2rst.py
diff options
context:
space:
mode:
Diffstat (limited to 'help2rst.py')
-rwxr-xr-xhelp2rst.py192
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())