[RFC PATCH v1 6/6] kernel-doc: add man page builder (target mandocs)

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This patch brings man page build we already know from the DocBook
toolchain. It adds a sphinx extension 'manKernelDoc' which consists of:

* '.. kernel-doc-man::' : directive implemented in class KernelDocMan
* 'kernel_doc_man'      : *invisible* node
* 'kernel-doc-man'      : an alternative man builder (class KernelDocManBuilder)

The 'kernel-doc-man' builder produces manual pages in the groff
format. It is a *man* page sphinx-builder mainly written to generate
manual pages from kernel-doc comments by:

* scanning the master doc-tree for sections marked with the
  '.. kernel-doc-man::' directive and build manual pages for theses
  sections.

* reorder / rename (sub-) sections according to the conventions that
  should be employed when writing man pages for the Linux man-pages
  project, see man-pages(7) ...  (has to be discussed if we really need
  such a reordering).

A few words about How the 'kernel-doc-man' builder an the kernel-doc
parser work together:

It starts with the kernel-doc parser which builds small reST-doctrees
from the comments of functions, structs and so on. The kernel-doc parser
has an option (see 'kernel_doc.ParseOptions.man_sect=n') which can be
activated e.g. on a *per* kernel-doc directive::

  .. kernel-doc:: include/linux/debugobjects.h
     :man-sect: 9

or alternative it can be activated globally with::

  kernel_doc_mansect = 9

in the conf.py (this is what this patch does).

With option 'man_sect=n' the kernel-doc parser inserts *invisible*
'kernel_doc_man' nodes in the reST-doctree. Here is a view on such a
doctree where the <kernel_doc_man/> is a child of the <section>
describing 'get_sd_load_idx' function::

  <section docname="basics" dupnames="get_sd_load_idx"
           ids="get-sd-load-idx id86" names="get_sd_load_idx">
    <title>get_sd_load_idx</title>
    <kernel_doc_man manpage="get_sd_load_idx.9"/>
    ...
    <desc desctype="function" domain="c" noindex="False" objtype="function">
      <desc_signature first="False" ids="c.get_sd_load_idx" names="c.get_sd_load_idx">
	<desc_type>int</desc_type>
        ...

After the whole doctree is build by sphinx-build it is stored in the env
and the builder takes place. These *invisible* 'kernel_doc_man' nodes
will be ignored by all builders (html, pdf etc.) except the
'kernel-doc-man' builder.  The later picks up those nodes from the
doctree and builds a manpage from the surrounding <section>.

Signed-off-by: Markus Heiser <markus.heiser@xxxxxxxxxxx>
---
 Documentation/Makefile.sphinx        |   5 +-
 Documentation/conf.py                |   3 +-
 Documentation/media/Makefile         |   1 +
 Documentation/sphinx/manKernelDoc.py | 408 +++++++++++++++++++++++++++++++++++
 4 files changed, 415 insertions(+), 2 deletions(-)
 create mode 100755 Documentation/sphinx/manKernelDoc.py

diff --git a/Documentation/Makefile.sphinx b/Documentation/Makefile.sphinx
index 626dfd0..73bd71b 100644
--- a/Documentation/Makefile.sphinx
+++ b/Documentation/Makefile.sphinx
@@ -89,10 +89,12 @@ epubdocs:
 xmldocs:
 	@$(foreach var,$(SPHINXDIRS),$(call loop_cmd,sphinx,xml,$(var),xml,$(var)))
 
+mandocs:
+	@$(foreach var,$(SPHINXDIRS),$(call loop_cmd,sphinx,kernel-doc-man,$(var),man,$(var)))
+
 # no-ops for the Sphinx toolchain
 sgmldocs:
 psdocs:
-mandocs:
 installmandocs:
 
 cleandocs:
@@ -106,6 +108,7 @@ dochelp:
 	@echo  '  htmldocs        - HTML'
 	@echo  '  latexdocs       - LaTeX'
 	@echo  '  pdfdocs         - PDF'
+	@echo  '  mandocs         - man pages'
 	@echo  '  epubdocs        - EPUB'
 	@echo  '  xmldocs         - XML'
 	@echo  '  cleandocs       - clean all generated files'
diff --git a/Documentation/conf.py b/Documentation/conf.py
index 013af9a..97b826b 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -35,7 +35,7 @@ from load_config import loadConfig
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = ['rstKernelDoc', 'rstFlatTable', 'kernel_include', 'cdomain',
-              'sphinx.ext.todo' ]
+              'manKernelDoc', 'sphinx.ext.todo' ]
 
 # The name of the math extension changed on Sphinx 1.4
 if major == 1 and minor > 3:
@@ -508,6 +508,7 @@ pdf_documents = [
 # line arguments.
 kernel_doc_verbose_warn = False
 kernel_doc_raise_error = False
+kernel_doc_mansect = 9
 
 # ------------------------------------------------------------------------------
 # Since loadConfig overwrites settings from the global namespace, it has to be
diff --git a/Documentation/media/Makefile b/Documentation/media/Makefile
index 3266360..289ca6d 100644
--- a/Documentation/media/Makefile
+++ b/Documentation/media/Makefile
@@ -103,6 +103,7 @@ html: all
 epub: all
 xml: all
 latex: $(IMGPDF) all
+kernel-doc-man: $(BUILDDIR) ${TARGETS}
 
 clean:
 	-rm -f $(DOTTGT) $(IMGTGT) ${TARGETS} 2>/dev/null
diff --git a/Documentation/sphinx/manKernelDoc.py b/Documentation/sphinx/manKernelDoc.py
new file mode 100755
index 0000000..3e27983
--- /dev/null
+++ b/Documentation/sphinx/manKernelDoc.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8; mode: python -*-
+
+u"""
+    kernel-doc-man
+    ~~~~~~~~~~~~~~
+
+    Implementation of the sphinx builder ``kernel-doc-man``.
+
+    :copyright:  Copyright (C) 2016  Markus Heiser
+    :license:    GPL Version 2, June 1991 see Linux/COPYING for details.
+
+    The ``kernel-doc-man`` (:py:class:`KernelDocManBuilder`) produces manual
+    pages in the groff format. It is a *man* page sphinx-builder mainly written
+    to generate manual pages from kernel-doc comments by:
+
+    * scanning the master doc-tree for sections marked with the
+      ``.. kernel-doc-man::`` directive and build manual pages for theses
+      sections.
+
+    * reorder / rename (sub-) sections according to the conventions that should
+      be employed when writing man pages for the Linux man-pages project, see
+      man-pages(7)
+
+    Usage::
+
+        $ sphinx-build -b kernel-doc-man
+
+    rest-Markup entry (e.g)::
+
+        .. kernel-doc-man::  manpage-name.9
+
+    Since the ``kernel-doc-man`` is an extension of the common `sphinx *man*
+    builder
+    <http://www.sphinx-doc.org/en/stable/config.html#confval-man_pages>`_, it is
+    also a full replacement, building booth, the common sphinx man-pages and
+    those marked with the ``.. kernel-doc-man::`` directive.
+
+    Mostly authors will use this feature in their reST documents in conjunction
+    with the ``.. kernel-doc::`` :ref:`directive
+    <kernel-doc:kernel-doc-directive>`, to create man pages from kernel-doc
+    comments.  This could be done, by setting the man section number with the
+    option ``man-sect``, e.g.::
+
+      .. kernel-doc:: include/linux/debugobjects.h
+          :man-sect: 9
+          :internal:
+
+    With this ``:man-sect: 9`` option, the kernel-doc parser will insert a
+    ``.. kernel-doc-man:: <declaration-name>.<man-sect no>`` directive in the
+    reST output, for every section describing a function, union etc.
+
+"""
+
+# ==============================================================================
+# imports
+# ==============================================================================
+
+import re
+import collections
+from os import path
+
+from docutils.io import FileOutput
+from docutils.frontend import OptionParser
+from docutils import nodes
+from docutils.utils import new_document
+from docutils.parsers.rst import Directive
+from docutils.transforms import Transform
+
+from sphinx import addnodes
+from sphinx.util.nodes import inline_all_toctrees
+from sphinx.util.console import bold, darkgreen     # pylint: disable=E0611
+from sphinx.writers.manpage import ManualPageWriter
+
+from sphinx.builders.manpage import ManualPageBuilder
+
+from kernel_doc import Container
+
+# ==============================================================================
+# common globals
+# ==============================================================================
+
+DEFAULT_MAN_SECT  = 9
+
+# The version numbering follows numbering of the specification
+# (Documentation/books/kernel-doc-HOWTO).
+__version__  = '1.0'
+
+# ==============================================================================
+def setup(app):
+# ==============================================================================
+
+    app.add_builder(KernelDocManBuilder)
+    app.add_directive("kernel-doc-man", KernelDocMan)
+    app.add_config_value('author', "", 'env')
+    app.add_node(kernel_doc_man
+                 , html    = (skip_kernel_doc_man, None)
+                 , latex   = (skip_kernel_doc_man, None)
+                 , texinfo = (skip_kernel_doc_man, None)
+                 , text    = (skip_kernel_doc_man, None)
+                 , man     = (skip_kernel_doc_man, None) )
+
+    return dict(
+        version = __version__
+        , parallel_read_safe = True
+        , parallel_write_safe = True
+    )
+
+# ==============================================================================
+class kernel_doc_man(nodes.Invisible, nodes.Element):    # pylint: disable=C0103
+# ==============================================================================
+    """Node to mark a section as *manpage*"""
+
+def skip_kernel_doc_man(self, node):                     # pylint: disable=W0613
+    raise nodes.SkipNode
+
+
+# ==============================================================================
+class KernelDocMan(Directive):
+# ==============================================================================
+
+    required_arguments = 1
+    optional_arguments = 0
+
+    def run(self):
+        man_node = kernel_doc_man()
+        man_node["manpage"] = self.arguments[0]
+        return [man_node]
+
+# ==============================================================================
+class Section2Manpage(Transform):
+# ==============================================================================
+    u"""Transforms a *section* tree into an *manpage* tree.
+
+    The structural layout of a man-page differs from the one produced, by the
+    kernel-doc parser. The kernel-doc parser produce reST which fits to *normal*
+    documentation, e.g. the declaration of a function in reST is like.
+
+    .. code-block:: rst
+
+        user_function
+        =============
+
+        .. c:function:: int user_function(int a)
+
+           The *purpose* description.
+
+           :param int a:
+               Parameter a description
+
+       Description
+       ===========
+
+       lorem ipsum ..
+
+       Return
+       ======
+
+       Returns first argument
+
+    On the other side, in man-pages it is common (see ``man man-pages``) to
+    print the *purpose* line in the "NAME" section, function's prototype in the
+    "SYNOPSIS" section and the parameter description in the "OPTIONS" section::
+
+       NAME
+              user_function -- The *purpose* description.
+
+       SYNOPSIS
+               int user_function(int a)
+
+       OPTIONS
+               a
+
+       DESCRIPTION
+               lorem ipsum
+
+       RETURN VALUE
+               Returns first argument
+
+    """
+    # The common section order is:
+    manTitles = [
+        (re.compile(r"^SYNOPSIS|^DEFINITION"
+                    , flags=re.I), "SYNOPSIS")
+        , (re.compile(r"^CONFIG",     flags=re.I), "CONFIGURATION")
+        , (re.compile(r"^DESCR",      flags=re.I), "DESCRIPTION")
+        , (re.compile(r"^OPTION",     flags=re.I), "OPTIONS")
+        , (re.compile(r"^EXIT",       flags=re.I), "EXIT STATUS")
+        , (re.compile(r"^RETURN",     flags=re.I), "RETURN VALUE")
+        , (re.compile(r"^ERROR",      flags=re.I), "ERRORS")
+        , (re.compile(r"^ENVIRON",    flags=re.I), "ENVIRONMENT")
+        , (re.compile(r"^FILE",       flags=re.I), "FILES")
+        , (re.compile(r"^VER",        flags=re.I), "VERSIONS")
+        , (re.compile(r"^ATTR",       flags=re.I), "ATTRIBUTES")
+        , (re.compile(r"^CONFOR",     flags=re.I), "CONFORMING TO")
+        , (re.compile(r"^NOTE",       flags=re.I), "NOTES")
+        , (re.compile(r"^BUG",        flags=re.I), "BUGS")
+        , (re.compile(r"^EXAMPLE",    flags=re.I), "EXAMPLE")
+        , (re.compile(r"^SEE",        flags=re.I), "SEE ALSO")
+        , ]
+
+    manTitleOrder = [t for r,t in manTitles]
+
+    @classmethod
+    def getFirstChild(cls, subtree, *classes):
+        for c in classes:
+            if subtree is None:
+                break
+            idx = subtree.first_child_matching_class(c)
+            if idx is None:
+                subtree = None
+                break
+            subtree = subtree[idx]
+        return subtree
+
+    def strip_man_info(self):
+        section  = self.document[0]
+        man_info = Container(authors=[])
+        man_node = self.getFirstChild(section, kernel_doc_man)
+        name, sect = (man_node["manpage"].split(".", -1) + [DEFAULT_MAN_SECT])[:2]
+        man_info["manpage"] = name
+        man_info["mansect"] = sect
+
+        # strip field list
+        field_list = self.getFirstChild(section, nodes.field_list)
+        if field_list:
+            field_list.parent.remove(field_list)
+            for field in field_list:
+                name  = field[0].astext().lower()
+                value = field[1].astext()
+                man_info[name] = man_info.get(name, []) + [value,]
+
+            # normalize authors
+            for auth, adr in zip(man_info.get("author", [])
+                                 , man_info.get("address", [])):
+                man_info["authors"].append("%s <%s>" % (auth, adr))
+
+        # strip *purpose*
+        desc_content = self.getFirstChild(
+            section, addnodes.desc, addnodes.desc_content)
+        if not len(desc_content):
+            # missing initial short description in kernel-doc comment
+            man_info.subtitle = ""
+        else:
+            man_info.subtitle = desc_content[0].astext()
+            del desc_content[0]
+
+        # remove section title
+        old_title = self.getFirstChild(section, nodes.title)
+        old_title.parent.remove(old_title)
+
+        # gather type of the declaration
+        decl_type = self.getFirstChild(
+            section, addnodes.desc, addnodes.desc_signature, addnodes.desc_type)
+        if decl_type is not None:
+            decl_type = decl_type.astext().strip()
+        man_info.decl_type = decl_type
+
+        # complete infos
+        man_info.title    = man_info["manpage"]
+        man_info.section  = man_info["mansect"]
+
+        return man_info
+
+    def isolateSections(self, sec_by_title):
+        section = self.document[0]
+        while True:
+            sect = self.getFirstChild(section, nodes.section)
+            if not sect:
+                break
+            sec_parent = sect.parent
+            target_idx = sect.parent.index(sect) - 1
+            sect.parent.remove(sect)
+            if isinstance(sec_parent[target_idx], nodes.target):
+                # drop target / is useless in man-pages
+                del sec_parent[target_idx]
+            title = sect[0].astext().upper()
+            for r, man_title in self.manTitles:
+                if r.search(title):
+                    title = man_title
+                    sect[0].replace_self(nodes.title(text = title))
+                    break
+            # we dont know if there are sections with the same title
+            sec_by_title[title] = sec_by_title.get(title, []) + [sect]
+
+        return sec_by_title
+
+    def isolateSynopsis(self, sec_by_title):
+        synopsis = None
+        c_desc = self.getFirstChild(self.document[0], addnodes.desc)
+        if c_desc is not None:
+            c_desc.parent.remove(c_desc)
+            synopsis = nodes.section()
+            synopsis += nodes.title(text = 'synopsis')
+            synopsis += c_desc
+            sec_by_title["SYNOPSIS"] = sec_by_title.get("SYNOPSIS", []) + [synopsis]
+        return sec_by_title
+
+    def apply(self):
+        self.document.man_info = self.strip_man_info()
+        sec_by_title = collections.OrderedDict()
+
+        self.isolateSections(sec_by_title)
+        # On struct, enum, union, typedef, the SYNOPSIS is taken from the
+        # DEFINITION section.
+        if self.document.man_info.decl_type not in [
+                "struct", "enum", "union", "typedef"]:
+            self.isolateSynopsis(sec_by_title)
+
+        for sec_name in self.manTitleOrder:
+            sec_list = sec_by_title.pop(sec_name,[])
+            self.document[0] += sec_list
+
+        for sec_list in sec_by_title.values():
+            self.document[0] += sec_list
+
+# ==============================================================================
+class KernelDocManBuilder(ManualPageBuilder):
+# ==============================================================================
+
+    """
+    Builds groff output in manual page format.
+    """
+    name = 'kernel-doc-man'
+    format = 'man'
+    supported_image_types = []
+
+    def init(self):
+        pass
+
+    def is_manpage(self, node):               # pylint: disable=R0201
+        if isinstance(node, nodes.section):
+            return bool(Section2Manpage.getFirstChild(
+            node, kernel_doc_man) is not None)
+        else:
+            return False
+
+    def prepare_writing(self, docnames):
+        """A place where you can add logic before :meth:`write_doc` is run"""
+        pass
+
+    def write_doc(self, docname, doctree):
+        """Where you actually write something to the filesystem."""
+        pass
+
+    def get_partial_document(self, children): # pylint: disable=R0201
+        doc_tree =  new_document('<output>')
+        doc_tree += children
+        return doc_tree
+
+    def write(self, *ignored):
+        if self.config.man_pages:
+            # build manpages from config.man_pages as usual
+            ManualPageBuilder.write(self, *ignored)
+            # FIXME:
+
+        self.info(bold("scan master tree for kernel-doc man-pages ... ") + darkgreen("{"), nonl=True)
+
+        master_tree = self.env.get_doctree(self.config.master_doc)
+        master_tree = inline_all_toctrees(
+            self, set(), self.config.master_doc, master_tree, darkgreen, [self.config.master_doc])
+        self.info(darkgreen("}"))
+        man_nodes   = master_tree.traverse(condition=self.is_manpage)
+        if not man_nodes and not self.config.man_pages:
+            self.warn('no "man_pages" config value nor manual section found; no manual pages '
+                      'will be written')
+            return
+
+        self.info(bold('writing man pages ... '), nonl=True)
+
+        for man_parent in man_nodes:
+
+            doc_tree = self.get_partial_document(man_parent)
+            Section2Manpage(doc_tree).apply()
+
+            if not doc_tree.man_info["authors"] and self.config.author:
+                doc_tree.man_info["authors"].append(self.config.author)
+
+            doc_writer   = ManualPageWriter(self)
+            doc_settings = OptionParser(
+                defaults            = self.env.settings
+                , components        = (doc_writer,)
+                , read_config_files = True
+                , ).get_default_values()
+
+            doc_settings.__dict__.update(doc_tree.man_info)
+            doc_tree.settings = doc_settings
+            targetname  = '%s.%s' % (doc_tree.man_info.title, doc_tree.man_info.section)
+            if doc_tree.man_info.decl_type in [
+                    "struct", "enum", "union", "typedef"]:
+                targetname = "%s_%s" % (doc_tree.man_info.decl_type, targetname)
+
+            destination = FileOutput(
+                destination_path = path.join(self.outdir, targetname)
+                , encoding='utf-8')
+
+            self.info(darkgreen(targetname) + " ", nonl=True)
+            self.env.resolve_references(doc_tree, doc_tree.man_info.manpage, self)
+
+            # remove pending_xref nodes
+            for pendingnode in doc_tree.traverse(addnodes.pending_xref):
+                pendingnode.replace_self(pendingnode.children)
+            doc_writer.write(doc_tree, destination)
+        self.info()
+
+
+    def finish(self):
+        pass
-- 
2.7.4

--
To unsubscribe from this list: send the line "unsubscribe linux-doc" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Kernel Newbies]     [Security]     [Netfilter]     [Bugtraq]     [Linux FS]     [Yosemite Forum]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Samba]     [Video 4 Linux]     [Device Mapper]     [Linux Resources]

  Powered by Linux