diff --git a/doc/toolchain/bin/help2adoc b/doc/toolchain/bin/help2adoc
new file mode 100755
index 0000000000..feeb5ebb62
--- /dev/null
+++ b/doc/toolchain/bin/help2adoc
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
+export PYTHONPATH=$(readlink -f "${SCRIPT_DIR}"/../lib)
+python3 -m help2adoc.main "$@"
diff --git a/doc/toolchain/lib/help2adoc/__init__.py b/doc/toolchain/lib/help2adoc/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/doc/toolchain/lib/help2adoc/generator.py b/doc/toolchain/lib/help2adoc/generator.py
new file mode 100644
index 0000000000..a30becc5b3
--- /dev/null
+++ b/doc/toolchain/lib/help2adoc/generator.py
@@ -0,0 +1,51 @@
+from .parser import Parser
+
+import typing
+import re
+
+
+class AsciiDocGenerator(Parser):
+ USAGE_RE = re.compile('^usage:\\s*')
+
+ def __init__(self, output: typing.Callable[[str], None]):
+ self.output = output
+
+ def on_usage(self, text: str):
+ usage = text[self.USAGE_RE.match(text).end():]
+ self.output('Usage:\n\n ')
+ self.output(usage)
+ self.output('\n\n')
+
+ def on_paragraph(self, text: str):
+ self.output(self.escape(text))
+ self.output('\n\n')
+
+ def enter_options(self):
+ self.output('Options:\n\n')
+
+ def exit_options(self):
+ self.output('\n')
+
+ def on_option(self, option: str, help: str):
+ self.output('* `+++')
+ self.output(option)
+ self.output('+++`: ')
+ self.output(self.escape(help))
+ self.output('\n\n')
+
+ def enter_option_group(self, name: str):
+ self.output(name)
+ self.output(':\n\n')
+
+ def exit_option_group(self, name: str):
+ self.output('\n')
+
+ def enter_description(self):
+ pass
+
+ def exit_description(self):
+ pass
+
+ def escape(self, text: str):
+ # TODO
+ return text
diff --git a/doc/toolchain/lib/help2adoc/main.py b/doc/toolchain/lib/help2adoc/main.py
new file mode 100644
index 0000000000..ca4526c597
--- /dev/null
+++ b/doc/toolchain/lib/help2adoc/main.py
@@ -0,0 +1,28 @@
+from .parser import lexer, LookAheadIterator
+from .generator import AsciiDocGenerator
+
+from argparse import ArgumentParser
+import sys
+
+def main():
+ ap = ArgumentParser('help2adoc')
+ ap.add_argument('file')
+ args = ap.parse_args()
+ with open(args.file, 'r') as f:
+ tokens = LookAheadIterator(lexer(f))
+ AsciiDocGenerator(sys.stdout.write).parse_help(tokens)
+ token = tokens.lookahead()
+ if token is None:
+ return
+ epilog_start = token.lineno
+ f.seek(0)
+ for i in range(epilog_start):
+ next(f)
+ print('....')
+ for line in f:
+ print(line, end='')
+ print('....')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/toolchain/lib/help2adoc/parser.py b/doc/toolchain/lib/help2adoc/parser.py
new file mode 100644
index 0000000000..d152417c2e
--- /dev/null
+++ b/doc/toolchain/lib/help2adoc/parser.py
@@ -0,0 +1,329 @@
+#!/usr/bin/env python3
+import typing
+import re
+import io
+
+
+class Token:
+ def __init__(self, lineno: int, tpe: str, text: str):
+ self.tpe = tpe
+ self.lineno = lineno
+ self.text = text
+
+ def __str__(self):
+ return f'Token(tpe={self.tpe}, text={self.text})'
+
+
+class EmptyLineToken(Token):
+ def __init__(self, lineno: int):
+ super().__init__(lineno, 'emptyline', '')
+
+
+class TextToken(Token):
+ def __init__(self, lineno: int, indent: int, text: str):
+ super().__init__(lineno, 'text', text)
+ self.indent = indent
+
+
+class LogToken(Token):
+ def __init__(self, lineno: int, text: str):
+ super().__init__(lineno, 'log', text)
+
+
+class UsageToken(Token):
+ def __init__(self, lineno: int, text: str):
+ super().__init__(lineno, 'usage', text)
+
+
+class OptionsToken(Token):
+ def __init__(self, lineno: int, text: str):
+ super().__init__(lineno, 'options', text)
+
+
+class OptionToken(Token):
+ def __init__(self, lineno: int, text: str):
+ super().__init__(lineno, 'option', text)
+
+
+def lexer(lines: typing.Iterator[str]):
+ for lineno, line in enumerate(lines):
+ line = line.rstrip()
+ if not line:
+ yield EmptyLineToken(lineno)
+ elif re.match('(?:\033\\[\\d+m)?(?:INFO|ERROR)(?:\033\\[0m)?: ', line):
+ pass
+ elif re.match('usage: ', line):
+ yield UsageToken(lineno, line)
+ elif re.match('options:', line):
+ yield OptionsToken(lineno, line)
+ elif re.match('\\s+-', line):
+ yield OptionToken(lineno, line)
+ else:
+ indent = re.search('\\S', line).start()
+ yield TextToken(lineno, indent, line)
+
+
+T = typing.TypeVar('T')
+
+
+class LookAheadIterator(typing.Generic[T]):
+ def __init__(self, it: typing.Iterator[T]):
+ self._it = it
+ self._lookahead_buffer = list()
+ self._lookahead_index = -1
+
+ def lookahead(self):
+ if self._lookahead_index + 1 == len(self._lookahead_buffer):
+ try:
+ ret = next(self._it)
+ self._lookahead_index += 1
+ self._lookahead_buffer.append(ret)
+ return ret
+ except StopIteration:
+ return None
+ else:
+ self._lookahead_index += 1
+ return self._lookahead_buffer[self._lookahead_index]
+
+ def consume(self):
+ self._lookahead_buffer = self._lookahead_buffer[self._lookahead_index+1:]
+ self._lookahead_index = -1
+
+ def rollback(self):
+ self._lookahead_index -= 1
+
+ def reset(self):
+ self._lookahead_index = -1
+
+
+class SyntaxErrorException(Exception):
+ pass
+
+
+class Parser:
+ def on_usage(self, text: str):
+ print('')
+
+ def enter_description(self):
+ print('')
+
+ def exit_description(self):
+ print('')
+
+ def on_paragraph(self, text: str):
+ print(f'{text}')
+
+ def enter_options(self):
+ print('')
+
+ def exit_options(self):
+ print('')
+
+ def enter_option_group(self, name: str):
+ print(f'')
+
+ def exit_option_group(self, name: str):
+ print(f'')
+
+ def on_option(self, option: str, help: str):
+ print(f'