]> git.ipfire.org Git - thirdparty/gnutls.git/commitdiff
doc: generate man-pages from JSON
authorDaiki Ueno <ueno@gnu.org>
Wed, 5 Jan 2022 06:24:03 +0000 (07:24 +0100)
committerDaiki Ueno <ueno@gnu.org>
Sat, 15 Jan 2022 08:25:56 +0000 (09:25 +0100)
This replaces man-pages generation previously provided by the autogen
-Tagman.tpl command with a Python script (gen-cmd-man.py).

Signed-off-by: Daiki Ueno <ueno@gnu.org>
doc/manpages/Makefile.am
doc/scripts/Makefile.am
doc/scripts/gen-cmd-man.py [new file with mode: 0644]

index c3bcac9f3bb96e720eda965f2cede34b7ac07080..8e1e25d06e1f7d8132df5e3353cd5cc0310a9d04 100644 (file)
@@ -43,61 +43,107 @@ endif
 EXTRA_DIST += $(TOOLS_MANS) $(SRP_MANS) $(DANE_MANS)
 MAINTAINERCLEANFILES += $(TOOLS_MANS) $(SRP_MANS) $(DANE_MANS)
 
-# Note that our .def files depend on autogen
-# supporting the @subheading texi keyword. This
-# is not currently the case so we do remove it
-# before processing. Once the new version of autogen
-# is out, replace the sed and tmp files with a simple
-# autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl $<
-certtool.1: ../../src/certtool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+$(man_MANS): $(top_srcdir)/doc/scripts/gen-cmd-man.py
 
-ocsptool.1: ../../src/ocsptool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+GEN_CMD_MAN_OPTIONS = \
+       --license=gpl3+ \
+       --version='$(VERSION)' \
+       --authors='Nikos Mavrogiannopoulos, Simon Josefsson and others; see /usr/share/doc/gnutls/AUTHORS for a complete list.' \
+       --copyright-year=2020-2021 \
+       --copyright-holder='Free Software Foundation, and others all rights reserved.' \
+       --bug-email=bugs@gnutls.org
 
-danetool.1: ../../src/danetool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+certtool.1: $(top_srcdir)/doc/certtool-see-also.texi $(top_srcdir)/doc/certtool-examples.texi $(top_srcdir)/doc/certtool-files.texi
+certtool.1: $(top_srcdir)/src/certtool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/certtool-see-also.texi \
+               --examples $(top_srcdir)/doc/certtool-examples.texi \
+               --files $(top_srcdir)/doc/certtool-files.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-gnutls-cli.1: ../../src/cli-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+ocsptool.1: $(top_srcdir)/doc/ocsptool-see-also.texi $(top_srcdir)/doc/ocsptool-examples.texi $(top_srcdir)/doc/ocsptool-description.texi
+ocsptool.1: $(top_srcdir)/src/ocsptool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/ocsptool-see-also.texi \
+               --examples $(top_srcdir)/doc/ocsptool-examples.texi \
+               --description $(top_srcdir)/doc/ocsptool-description.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-gnutls-serv.1: ../../src/serv-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+danetool.1: $(top_srcdir)/doc/danetool-see-also.texi $(top_srcdir)/doc/danetool-examples.texi
+danetool.1: $(top_srcdir)/src/danetool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/danetool-see-also.texi \
+               --examples $(top_srcdir)/doc/danetool-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-gnutls-cli-debug.1: ../../src/cli-debug-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+gnutls-cli.1: $(top_srcdir)/doc/gnutls-cli-see-also.texi $(top_srcdir)/doc/gnutls-cli-examples.texi
+gnutls-cli.1: $(top_srcdir)/src/gnutls-cli-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/gnutls-cli-see-also.texi \
+               --examples $(top_srcdir)/doc/gnutls-cli-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-srptool.1: ../../src/srptool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+gnutls-serv.1: $(top_srcdir)/doc/gnutls-serv-see-also.texi $(top_srcdir)/doc/gnutls-serv-examples.texi
+gnutls-serv.1: $(top_srcdir)/src/gnutls-serv-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/gnutls-serv-see-also.texi \
+               --examples $(top_srcdir)/doc/gnutls-serv-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-p11tool.1: ../../src/p11tool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+gnutls-cli-debug.1: $(top_srcdir)/doc/gnutls-cli-debug-see-also.texi $(top_srcdir)/doc/gnutls-cli-debug-examples.texi
+gnutls-cli-debug.1: $(top_srcdir)/src/gnutls-cli-debug-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/gnutls-cli-debug-see-also.texi \
+               --examples $(top_srcdir)/doc/gnutls-cli-debug-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-tpmtool.1: ../../src/tpmtool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+srptool.1: $(top_srcdir)/doc/srptool-see-also.texi $(top_srcdir)/doc/srptool-examples.texi
+srptool.1: $(top_srcdir)/src/srptool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/srptool-see-also.texi \
+               --examples $(top_srcdir)/doc/srptool-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
-psktool.1: ../../src/psktool-args.def
-       -sed 's/@subheading \(.*\)/@*\n@var{\1}\n@*/' $< > "$<".tmp && \
-       autogen -L ../../src -DMAN_SECTION=1 -Tagman-cmd.tpl "$<".tmp && \
-       rm -f "$<".tmp
+p11tool.1: $(top_srcdir)/doc/p11tool-see-also.texi $(top_srcdir)/doc/p11tool-examples.texi
+p11tool.1: $(top_srcdir)/src/p11tool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/p11tool-see-also.texi \
+               --examples $(top_srcdir)/doc/p11tool-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
+
+tpmtool.1: $(top_srcdir)/doc/tpmtool-see-also.texi $(top_srcdir)/doc/tpmtool-examples.texi
+tpmtool.1: $(top_srcdir)/src/tpmtool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/tpmtool-see-also.texi \
+               --examples $(top_srcdir)/doc/tpmtool-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
+
+psktool.1: $(top_srcdir)/doc/psktool-see-also.texi $(top_srcdir)/doc/psktool-examples.texi
+psktool.1: $(top_srcdir)/src/psktool-options.json
+       $(AM_V_GEN) PYTHONPATH='$(top_srcdir)/python' \
+               $(PYTHON) $(top_srcdir)/doc/scripts/gen-cmd-man.py \
+               --see-also $(top_srcdir)/doc/psktool-see-also.texi \
+               --examples $(top_srcdir)/doc/psktool-examples.texi \
+               $(GEN_CMD_MAN_OPTIONS) \
+               $< $@
 
 APIMANS =
 APIMANS += dane_cert_type_name.3
index a310b626cec5623a89e691337852685d5cae791c..f1992c73fe08b7a45738548533141348ce358625 100644 (file)
@@ -17,4 +17,4 @@
 # along with this file; if not, write to the Free Software Foundation,
 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
-EXTRA_DIST = gdoc sort2.pl split-texi.pl getfuncs.pl getfuncs-map.pl gen-cmd-texi.py
+EXTRA_DIST = gdoc sort2.pl split-texi.pl getfuncs.pl getfuncs-map.pl gen-cmd-texi.py gen-cmd-man.py
diff --git a/doc/scripts/gen-cmd-man.py b/doc/scripts/gen-cmd-man.py
new file mode 100644 (file)
index 0000000..dbff09f
--- /dev/null
@@ -0,0 +1,348 @@
+#!/usr/bin/python
+# Copyright (C) 2021 Daiki Ueno
+
+# This file is part of GnuTLS.
+
+# GnuTLS is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# GnuTLS is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see
+# <https://www.gnu.org/licenses/>.
+
+from typing import Mapping, Optional, TextIO, Sequence
+import datetime
+import io
+import re
+import jsonopts
+
+
+def gen_option_docs(meta: Mapping[str, str],
+                    options: Sequence[Mapping[str, str]]) -> str:
+    docs = io.StringIO()
+    for option in options:
+        long_opt = option['long-option']
+        long_opt_escaped = long_opt.replace('-', '\\-')
+        short_opt = option.get('short-option')
+        detail = option.get('detail')
+        desc = option.get('desc')
+        disable_prefix = option.get('disable-prefix')
+        if disable_prefix:
+            disable_opt: Optional[str] = f'{disable_prefix}{long_opt}'
+        else:
+            disable_opt = None
+        alias = option.get('aliases')
+        if alias:
+            docs.write(f'''\
+.TP
+.NOP \\f\\*[B-Font]\\-\\-{long_opt_escaped}\\f[]
+This is an alias for the \\fI--{alias}\\fR option.
+''')
+            if 'deprecated' in option:
+                docs.write('''\
+.sp
+.B
+NOTE: THIS OPTION IS DEPRECATED
+''')
+            continue
+
+        arg_type = option.get('arg-type')
+        if arg_type:
+            arg_name = option.get('arg-name', arg_type).lower()
+            arg = f'\\f\\*[I-Font]{arg_name}\\f[]'
+            long_arg = f'={arg}'
+            short_arg = f' {arg}'
+        else:
+            long_arg = ''
+            short_arg = ''
+        formatted_options = list()
+        if short_opt:
+            formatted_options.append(
+                f'\\f\\*[B-Font]\\-{short_opt}\\f[]{short_arg}'
+            )
+        formatted_options.append(
+            f'\\f\\*[B-Font]\\-\\-{long_opt_escaped}\\f[]{long_arg}'
+        )
+        if disable_opt:
+            disable_opt_escaped = disable_opt.replace('-', '\\-')
+            formatted_options.append(
+                f'\\f\\*[B-Font]\\-\\-{disable_opt_escaped}\\f[]'
+            )
+        docs.write(f'''\
+.TP
+.NOP {', '.join(formatted_options)}
+''')
+        if desc and desc[0].isupper():
+            docs.write(f'{desc}.\n')
+        if 'stack-arg' in option:
+            docs.write(
+                'This option may appear an unlimited number of times.\n'
+            )
+        if arg_type == 'number':
+            docs.write(
+                'This option takes an integer number as its argument.\n'
+            )
+            arg_min = option.get('arg-min')
+            arg_max = option.get('arg-max')
+            if arg_min and arg_max:
+                docs.write(f'''\
+The value of
+\\f\\*[I-Font]{arg_name}\\f[]
+is constrained to being:
+.in +4
+.nf
+.na
+in the range {arg_min} through {arg_max}
+.fi
+.in -4
+''')
+        conflict_opts = option.get('conflicts', '').split()
+        if len(conflict_opts) > 0:
+            docs.write(f'''\
+This option must not appear in combination with any of the following options:
+{', '.join(conflict_opts)}.
+''')
+        require_opts = option.get('requires', '').split()
+        if len(require_opts) > 0:
+            docs.write(f'''\
+This option must appear in combination with the following options:
+{', '.join(require_opts)}.
+''')
+        if disable_opt:
+            disable_opt_escaped = disable_opt.replace('-', '\\-')
+            docs.write((
+                f'The \\fI{disable_opt_escaped}\\fP form '
+                'will disable the option.\n'
+            ))
+        if 'enabled' in option:
+            docs.write('This option is enabled by default.\n')
+        if desc and desc[0].isupper():
+            docs.write('.sp\n')
+        if detail:
+            docs.write(f'{text_to_man(detail)}\n')
+        if 'deprecated' in option:
+            docs.write('''\
+.sp
+.B
+NOTE: THIS OPTION IS DEPRECATED
+''')
+    return docs.getvalue()
+
+
+def text_to_man(s: str) -> str:
+    s = re.sub(r'-', r'\\-', s)
+    s = re.sub(r'(?m)^$', r'.sp', s)
+    s = re.sub(r"``(.*)''", r'\\(lq\1\\(rq', s)
+    return s
+
+
+def texi_to_man(s: str) -> str:
+    s = text_to_man(s)
+    s = re.sub(r'@([{}@])', r'\1', s)
+    s = re.sub(r'@code\{(.*?)\}', r'\\fB\1\\fP', s)
+    s = re.sub(r'@file\{(.*?)\}', r'\\fI\1\\fP', s)
+    s = re.sub(r'@subheading (.*)', r'''.br
+\\fB\1\\fP
+.br''', s)
+    s = re.sub(r'@example', r'''.br
+.in +4
+.nf''', s)
+    s = re.sub(r'@end example', r'''.in -4
+.fi''', s)
+    return s
+
+
+def include(name: str, includes: Mapping[str, TextIO]) -> str:
+    docs = io.StringIO()
+    f = includes.get(name)
+    if f:
+        docs.write(texi_to_man(f.read().strip()))
+    return docs.getvalue()
+
+
+LICENSES = {
+    'gpl3+': 'the GNU General Public License, version 3 or later',
+}
+
+
+def gen(infile: TextIO,
+        meta: Mapping[str, str],
+        includes: Mapping[str, TextIO],
+        man: TextIO):
+    sections = [jsonopts.Section.from_json(section)
+                for section in json.load(args.json)]
+    sections.append(jsonopts.Section.default())
+
+    prog_name = sections[0].meta['prog-name']
+    prog_title = sections[0].meta['prog-title']
+    argument = sections[0].meta.get('argument')
+    authors = meta.get('authors', 'AUTHORS')
+    copyright_year = meta.get('copyright-year',
+                              str(datetime.date.today().year))
+    copyright_holder = meta.get('copyright-holder', 'COPYRIGHT HOLDER')
+    license_text = LICENSES.get(meta['license'])
+    version = meta.get('version', '')
+    description = includes.get('description')
+    if description:
+        detail = texi_to_man(description.read())
+    else:
+        detail = sections[0].meta['detail']
+
+    section_docs = io.StringIO()
+    for section in sections:
+        section_id = section.meta.get('id', '')
+        if section_id:
+            section_desc = section.meta['desc']
+            option_docs = gen_option_docs(sections[0].meta, section.options)
+            section_docs.write(f'''\
+.SS "{section_desc}"
+{option_docs}\
+''')
+        else:
+            section_docs.write(gen_option_docs(sections[0].meta,
+                                               section.options))
+
+    formatted_date = datetime.date.today().strftime('%d %b %Y')
+    detail_concatenated = '\n.sp\n'.join(detail.strip().split('\n\n'))
+    man.write(f'''\
+.de1 NOP
+.  it 1 an-trap
+.  if \\\\n[.$] \\,\\\\$*\\/
+..
+.ie t \\
+.ds B-Font [CB]
+.ds I-Font [CI]
+.ds R-Font [CR]
+.el \\
+.ds B-Font B
+.ds I-Font I
+.ds R-Font R
+.TH {prog_name} 1 "{formatted_date}" "{version}" "User Commands"
+.SH NAME
+\\f\\*[B-Font]{prog_name}\\fP
+\\- {prog_title}
+.SH SYNOPSIS
+\\f\\*[B-Font]{prog_name}\\fP
+.\\" Mixture of short (flag) options and long options
+[\\f\\*[B-Font]\\-flags\\f[]]
+[\\f\\*[B-Font]\\-flag\\f[] [\\f\\*[I-Font]value\\f[]]]
+[\\f\\*[B-Font]\\-\\-option-name\\f[][[=| ]\\f\\*[I-Font]value\\f[]]]
+''')
+    if argument:
+        man.write(f'''\
+{argument}
+.sp \\n(Ppu
+.ne 2
+
+Operands and options may be intermixed.  They will be reordered.
+.sp \\n(Ppu
+.ne 2
+''')
+    else:
+        man.write('''\
+.sp \\n(Ppu
+.ne 2
+
+All arguments must be options.
+.sp \\n(Ppu
+.ne 2
+''')
+    man.write(f'''\
+.SH "DESCRIPTION"
+{detail_concatenated}
+.sp
+.SH "OPTIONS"
+{section_docs.getvalue()}
+''')
+    if 'files' in includes:
+        man.write(f'''\
+.SH FILES
+{include('files', includes)}
+''')
+    if 'examples' in includes:
+        man.write(f'''\
+.sp
+.SH EXAMPLES
+{include('examples', includes)}
+''')
+    man.write('''\
+.SH "EXIT STATUS"
+One of the following exit values will be returned:
+.TP
+.NOP 0 " (EXIT_SUCCESS)"
+Successful program execution.
+.TP
+.NOP 1 " (EXIT_FAILURE)"
+The operation failed or the command syntax was not valid.
+.PP
+''')
+    if 'see-also' in includes:
+        man.write(f'''\
+.SH "SEE ALSO"
+{include('see-also', includes)}
+''')
+    man.write(f'''\
+.SH "AUTHORS"
+{authors}
+.SH "COPYRIGHT"
+Copyright (C) {copyright_year} {copyright_holder}
+This program is released under the terms of {license_text}.
+''')
+    bug_email = meta.get('bug-email')
+    if bug_email:
+        man.write(f'''\
+.SH "BUGS"
+Please send bug reports to: {bug_email}
+''')
+
+
+if __name__ == '__main__':
+    import argparse
+    import json
+
+    parser = argparse.ArgumentParser(description='generate man-page')
+    parser.add_argument('json', type=argparse.FileType('r'))
+    parser.add_argument('man', type=argparse.FileType('w'))
+    parser.add_argument('--description', type=argparse.FileType('r'))
+    parser.add_argument('--see-also', type=argparse.FileType('r'))
+    parser.add_argument('--examples', type=argparse.FileType('r'))
+    parser.add_argument('--files', type=argparse.FileType('r'))
+    parser.add_argument('--authors', help='authors')
+    parser.add_argument('--bug-email', help='bug report email address')
+    parser.add_argument('--copyright-year', help='copyright year')
+    parser.add_argument('--copyright-holder', help='copyright holder')
+    parser.add_argument('--license', help='license')
+    parser.add_argument('--version', help='version')
+
+    args = parser.parse_args()
+    includes = dict()
+    if args.see_also:
+        includes['see-also'] = args.see_also
+    if args.examples:
+        includes['examples'] = args.examples
+    if args.files:
+        includes['files'] = args.files
+    if args.description:
+        includes['description'] = args.description
+    meta = dict()
+    if args.authors:
+        meta['authors'] = args.authors
+    if args.bug_email:
+        meta['bug-email'] = args.bug_email
+    if args.copyright_year:
+        meta['copyright-year'] = args.copyright_year
+    if args.copyright_holder:
+        meta['copyright-holder'] = args.copyright_holder
+    if args.license:
+        meta['license'] = args.license
+    if args.version:
+        meta['version'] = args.version
+
+    gen(args.json, meta, includes, args.man)