mirror of
https://github.com/ansible/ansible-documentation.git
synced 2026-03-26 13:18:58 +07:00
Use the new CLI docs generator from ansible-core (#220)
This tool provides a stable interface for generating RST documentation for ansible-core CLI programs. It avoids having the docs generation process in this repo depending on ansible-core internals that are subject to change. Relates: https://github.com/ansible/ansible/pull/81395
This commit is contained in:
@@ -2,7 +2,7 @@ OS := $(shell uname -s)
|
|||||||
PLUGIN_FORMATTER=../../hacking/build-ansible.py docs-build
|
PLUGIN_FORMATTER=../../hacking/build-ansible.py docs-build
|
||||||
KEYWORD_DUMPER=../../hacking/build-ansible.py document-keywords
|
KEYWORD_DUMPER=../../hacking/build-ansible.py document-keywords
|
||||||
CONFIG_DUMPER=../../hacking/build-ansible.py document-config
|
CONFIG_DUMPER=../../hacking/build-ansible.py document-config
|
||||||
GENERATE_CLI=../../hacking/build-ansible.py generate-man
|
GENERATE_CLI=../../packaging/cli-doc/build.py rst
|
||||||
COLLECTION_DUMPER=../../hacking/build-ansible.py collection-meta
|
COLLECTION_DUMPER=../../hacking/build-ansible.py collection-meta
|
||||||
ifeq ($(shell echo $(OS) | grep -Eic 'Darwin|FreeBSD|OpenBSD|DragonFly'),1)
|
ifeq ($(shell echo $(OS) | grep -Eic 'Darwin|FreeBSD|OpenBSD|DragonFly'),1)
|
||||||
CPUS ?= $(shell sysctl hw.ncpu|awk '{print $$2}')
|
CPUS ?= $(shell sysctl hw.ncpu|awk '{print $$2}')
|
||||||
@@ -194,10 +194,8 @@ clean:
|
|||||||
collections_meta: ../templates/collections_galaxy_meta.rst.j2
|
collections_meta: ../templates/collections_galaxy_meta.rst.j2
|
||||||
$(COLLECTION_DUMPER) --template-file=../templates/collections_galaxy_meta.rst.j2 --output-dir=rst/dev_guide/ $(EXTRA_COLLECTION_META_ARGS) ../../lib/ansible/galaxy/data/collections_galaxy_meta.yml
|
$(COLLECTION_DUMPER) --template-file=../templates/collections_galaxy_meta.rst.j2 --output-dir=rst/dev_guide/ $(EXTRA_COLLECTION_META_ARGS) ../../lib/ansible/galaxy/data/collections_galaxy_meta.yml
|
||||||
|
|
||||||
# TODO: make generate_man output dir cli option
|
|
||||||
cli:
|
cli:
|
||||||
mkdir -p rst/cli
|
$(GENERATE_CLI) --output-dir=rst/cli/
|
||||||
$(GENERATE_CLI) --template-file=../templates/cli_rst.j2 --output-dir=rst/cli/ --output-format rst $(EXTRA_CLI_DUMPER_ARGS) ../../lib/ansible/cli/*.py
|
|
||||||
|
|
||||||
keywords: ../templates/playbooks_keywords.rst.j2
|
keywords: ../templates/playbooks_keywords.rst.j2
|
||||||
$(KEYWORD_DUMPER) --template-dir=../templates --output-dir=rst/reference_appendices/ ../../lib/ansible/keyword_desc.yml $(EXTRA_KEYWORD_DUMPER_ARGS)
|
$(KEYWORD_DUMPER) --template-dir=../templates --output-dir=rst/reference_appendices/ ../../lib/ansible/keyword_desc.yml $(EXTRA_KEYWORD_DUMPER_ARGS)
|
||||||
|
|||||||
161
docs/templates/cli_rst.j2
vendored
161
docs/templates/cli_rst.j2
vendored
@@ -1,161 +0,0 @@
|
|||||||
:source: {{ source }}
|
|
||||||
|
|
||||||
{% set name = cli_name -%}
|
|
||||||
{% set name_slug = cli_name -%}
|
|
||||||
|
|
||||||
.. _{{name}}:
|
|
||||||
|
|
||||||
{% set name_len = name|length + 0-%}
|
|
||||||
{{ '=' * name_len }}
|
|
||||||
{{name}}
|
|
||||||
{{ '=' * name_len }}
|
|
||||||
|
|
||||||
|
|
||||||
:strong:`{{short_desc|default('')}}`
|
|
||||||
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:local:
|
|
||||||
:depth: {{content_depth}}
|
|
||||||
|
|
||||||
|
|
||||||
.. program:: {{cli_name}}
|
|
||||||
|
|
||||||
Synopsis
|
|
||||||
========
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
{{ usage|replace('%prog', cli_name) }}
|
|
||||||
|
|
||||||
|
|
||||||
Description
|
|
||||||
===========
|
|
||||||
|
|
||||||
|
|
||||||
{{ long_desc|default('', True) }}
|
|
||||||
|
|
||||||
{% if options %}
|
|
||||||
Common Options
|
|
||||||
==============
|
|
||||||
|
|
||||||
|
|
||||||
{% for option in options|sort(attribute='options') if option.options %}
|
|
||||||
|
|
||||||
.. option:: {% for switch in option['options'] %}{{switch}}{% if option['arg'] %} <{{option['arg']}}>{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
|
|
||||||
|
|
||||||
{{ option['desc'] }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if arguments %}
|
|
||||||
ARGUMENTS
|
|
||||||
=========
|
|
||||||
|
|
||||||
.. program:: {{cli_name}}
|
|
||||||
|
|
||||||
{% for arg in arguments %}
|
|
||||||
.. option:: {{ arg }}
|
|
||||||
|
|
||||||
{{ (arguments[arg]|default(' '))}}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if actions %}
|
|
||||||
Actions
|
|
||||||
=======
|
|
||||||
|
|
||||||
{% for action in actions %}
|
|
||||||
|
|
||||||
.. program:: {{cli_name}} {{action}}
|
|
||||||
.. _{{cli_name|replace('-','_')}}_{{action}}:
|
|
||||||
|
|
||||||
{{ action}}
|
|
||||||
{{ '-' * action|length}}
|
|
||||||
|
|
||||||
{{ (actions[action]['desc']|default(' '))}}
|
|
||||||
|
|
||||||
{% if actions[action]['options'] %}
|
|
||||||
|
|
||||||
|
|
||||||
{% for option in actions[action]['options']|sort(attribute='options') %}
|
|
||||||
.. option:: {% for switch in option['options'] if switch in actions[action]['option_names'] %}{{switch}} {% if option['arg'] %} <{{option['arg']}}>{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
|
|
||||||
|
|
||||||
{{ (option['desc']) }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for sub_action in actions[action]['actions'] %}
|
|
||||||
|
|
||||||
|
|
||||||
.. program:: {{cli_name}} {{action}} {{sub_action}}
|
|
||||||
.. _{{cli_name|replace('-','_')}}_{{action}}_{{sub_action}}:
|
|
||||||
|
|
||||||
{{ action + " " + sub_action }}
|
|
||||||
{{ '+' * (action|length + sub_action|length + 1) }}
|
|
||||||
|
|
||||||
{{ (actions[action]['actions'][sub_action]['desc']|default(' '))}}
|
|
||||||
|
|
||||||
{% if actions[action]['actions'][sub_action]['options'] %}
|
|
||||||
|
|
||||||
|
|
||||||
{% for option in actions[action]['actions'][sub_action]['options']|sort(attribute='options') %}
|
|
||||||
.. option:: {% for switch in option['options'] if switch in actions[action]['actions'][sub_action]['option_names'] %}{{switch}} {% if option['arg'] %} <{{option['arg']}}>{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
|
|
||||||
|
|
||||||
{{ (option['desc']) }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
.. program:: {{cli_name}}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
Environment
|
|
||||||
===========
|
|
||||||
|
|
||||||
The following environment variables may be specified.
|
|
||||||
|
|
||||||
{% if inventory %}
|
|
||||||
:envvar:`ANSIBLE_INVENTORY` -- Override the default ansible inventory file
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% if library %}
|
|
||||||
:envvar:`ANSIBLE_LIBRARY` -- Override the default ansible module library path
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
:envvar:`ANSIBLE_CONFIG` -- Override the default ansible config file
|
|
||||||
|
|
||||||
Many more are available for most options in ansible.cfg
|
|
||||||
|
|
||||||
|
|
||||||
Files
|
|
||||||
=====
|
|
||||||
|
|
||||||
{% if inventory %}
|
|
||||||
:file:`/etc/ansible/hosts` -- Default inventory file
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
:file:`/etc/ansible/ansible.cfg` -- Config file, used if present
|
|
||||||
|
|
||||||
:file:`~/.ansible.cfg` -- User config file, overrides the default config if present
|
|
||||||
|
|
||||||
Author
|
|
||||||
======
|
|
||||||
|
|
||||||
Ansible was originally written by Michael DeHaan.
|
|
||||||
|
|
||||||
See the `AUTHORS` file for a complete list of contributors.
|
|
||||||
|
|
||||||
|
|
||||||
License
|
|
||||||
=======
|
|
||||||
|
|
||||||
Ansible is released under the terms of the GPLv3+ License.
|
|
||||||
|
|
||||||
See also
|
|
||||||
========
|
|
||||||
|
|
||||||
{% for other in cli_bin_name_list|sort %}{% if other != cli_name %}:manpage:`{{other}}(1)`{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
# Copyright: (c) 2019, Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
|
|
||||||
# Make coding more python3-ish
|
|
||||||
from __future__ import (absolute_import, division, print_function)
|
|
||||||
__metaclass__ = type
|
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os.path
|
|
||||||
import pathlib
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
|
|
||||||
from ansible.module_utils.common.text.converters import to_bytes
|
|
||||||
|
|
||||||
# Pylint doesn't understand Python3 namespace modules.
|
|
||||||
from ..change_detection import update_file_if_different # pylint: disable=relative-beyond-top-level
|
|
||||||
from ..commands import Command # pylint: disable=relative-beyond-top-level
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TEMPLATE_FILE = pathlib.Path(__file__).parents[4] / 'hacking/templates/man.j2'
|
|
||||||
|
|
||||||
|
|
||||||
# from https://www.python.org/dev/peps/pep-0257/
|
|
||||||
def trim_docstring(docstring):
|
|
||||||
if not docstring:
|
|
||||||
return ''
|
|
||||||
# Convert tabs to spaces (following the normal Python rules)
|
|
||||||
# and split into a list of lines:
|
|
||||||
lines = docstring.expandtabs().splitlines()
|
|
||||||
# Determine minimum indentation (first line doesn't count):
|
|
||||||
indent = sys.maxsize
|
|
||||||
for line in lines[1:]:
|
|
||||||
stripped = line.lstrip()
|
|
||||||
if stripped:
|
|
||||||
indent = min(indent, len(line) - len(stripped))
|
|
||||||
# Remove indentation (first line is special):
|
|
||||||
trimmed = [lines[0].strip()]
|
|
||||||
if indent < sys.maxsize:
|
|
||||||
for line in lines[1:]:
|
|
||||||
trimmed.append(line[indent:].rstrip())
|
|
||||||
# Strip off trailing and leading blank lines:
|
|
||||||
while trimmed and not trimmed[-1]:
|
|
||||||
trimmed.pop()
|
|
||||||
while trimmed and not trimmed[0]:
|
|
||||||
trimmed.pop(0)
|
|
||||||
# Return a single string:
|
|
||||||
return '\n'.join(trimmed)
|
|
||||||
|
|
||||||
|
|
||||||
def get_options(optlist):
|
|
||||||
''' get actual options '''
|
|
||||||
|
|
||||||
opts = []
|
|
||||||
for opt in optlist:
|
|
||||||
if opt.help == argparse.SUPPRESS:
|
|
||||||
continue
|
|
||||||
res = {
|
|
||||||
'desc': opt.help,
|
|
||||||
'options': opt.option_strings
|
|
||||||
}
|
|
||||||
if isinstance(opt, argparse._StoreAction):
|
|
||||||
res['arg'] = opt.dest.upper()
|
|
||||||
elif not res['options']:
|
|
||||||
continue
|
|
||||||
opts.append(res)
|
|
||||||
|
|
||||||
return opts
|
|
||||||
|
|
||||||
|
|
||||||
def dedupe_groups(parser):
|
|
||||||
action_groups = []
|
|
||||||
for action_group in parser._action_groups:
|
|
||||||
found = False
|
|
||||||
for a in action_groups:
|
|
||||||
if a._actions == action_group._actions:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
action_groups.append(action_group)
|
|
||||||
return action_groups
|
|
||||||
|
|
||||||
|
|
||||||
def get_option_groups(option_parser):
|
|
||||||
groups = []
|
|
||||||
for action_group in dedupe_groups(option_parser)[1:]:
|
|
||||||
group_info = {}
|
|
||||||
group_info['desc'] = action_group.description
|
|
||||||
group_info['options'] = action_group._actions
|
|
||||||
group_info['group_obj'] = action_group
|
|
||||||
groups.append(group_info)
|
|
||||||
return groups
|
|
||||||
|
|
||||||
|
|
||||||
def opt_doc_list(parser):
|
|
||||||
''' iterate over options lists '''
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for option_group in dedupe_groups(parser)[1:]:
|
|
||||||
results.extend(get_options(option_group._actions))
|
|
||||||
|
|
||||||
results.extend(get_options(parser._actions))
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# def opts_docs(cli, name):
|
|
||||||
def opts_docs(cli_class_name, cli_module_name):
|
|
||||||
''' generate doc structure from options '''
|
|
||||||
|
|
||||||
cli_name = 'ansible-%s' % cli_module_name
|
|
||||||
if cli_module_name == 'adhoc':
|
|
||||||
cli_name = 'ansible'
|
|
||||||
|
|
||||||
# WIth no action/subcommand
|
|
||||||
# shared opts set
|
|
||||||
# instantiate each cli and ask its options
|
|
||||||
cli_klass = getattr(__import__("ansible.cli.%s" % cli_module_name,
|
|
||||||
fromlist=[cli_class_name]), cli_class_name)
|
|
||||||
cli = cli_klass([cli_name])
|
|
||||||
|
|
||||||
# parse the common options
|
|
||||||
try:
|
|
||||||
cli.init_parser()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# base/common cli info
|
|
||||||
cli_options = opt_doc_list(cli.parser)
|
|
||||||
docs = {
|
|
||||||
'source': f'lib/ansible/cli/{cli_module_name}.py',
|
|
||||||
'cli_name': cli_name,
|
|
||||||
'usage': cli.parser.format_usage(),
|
|
||||||
'short_desc': cli.parser.description,
|
|
||||||
'long_desc': trim_docstring(cli.__doc__),
|
|
||||||
'actions': {},
|
|
||||||
'content_depth': 2,
|
|
||||||
'options': cli_options,
|
|
||||||
'arguments': getattr(cli, 'ARGUMENTS', None),
|
|
||||||
}
|
|
||||||
option_info = {'option_names': [],
|
|
||||||
'options': cli_options,
|
|
||||||
'groups': []}
|
|
||||||
|
|
||||||
groups_info = get_option_groups(cli.parser)
|
|
||||||
shared_opt_names = []
|
|
||||||
for opt in cli_options:
|
|
||||||
shared_opt_names.extend(opt.get('options', []))
|
|
||||||
|
|
||||||
option_info['option_names'] = shared_opt_names
|
|
||||||
|
|
||||||
option_info['groups'].extend(groups_info)
|
|
||||||
|
|
||||||
docs.update(option_info)
|
|
||||||
|
|
||||||
# now for each action/subcommand
|
|
||||||
# force populate parser with per action options
|
|
||||||
|
|
||||||
def get_actions(parser, docs):
|
|
||||||
# use class attrs not the attrs on a instance (not that it matters here...)
|
|
||||||
try:
|
|
||||||
subparser = parser._subparsers._group_actions[0].choices
|
|
||||||
except AttributeError:
|
|
||||||
subparser = {}
|
|
||||||
|
|
||||||
depth = 0
|
|
||||||
|
|
||||||
for action, parser in subparser.items():
|
|
||||||
action_info = {'option_names': [],
|
|
||||||
'options': [],
|
|
||||||
'actions': {}}
|
|
||||||
# docs['actions'][action] = {}
|
|
||||||
# docs['actions'][action]['name'] = action
|
|
||||||
action_info['name'] = action
|
|
||||||
action_info['desc'] = trim_docstring(getattr(cli, 'execute_%s' % action).__doc__)
|
|
||||||
|
|
||||||
# docs['actions'][action]['desc'] = getattr(cli, 'execute_%s' % action).__doc__.strip()
|
|
||||||
action_doc_list = opt_doc_list(parser)
|
|
||||||
|
|
||||||
uncommon_options = []
|
|
||||||
for action_doc in action_doc_list:
|
|
||||||
# uncommon_options = []
|
|
||||||
|
|
||||||
option_aliases = action_doc.get('options', [])
|
|
||||||
for option_alias in option_aliases:
|
|
||||||
|
|
||||||
if option_alias in shared_opt_names:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO: use set
|
|
||||||
if option_alias not in action_info['option_names']:
|
|
||||||
action_info['option_names'].append(option_alias)
|
|
||||||
|
|
||||||
if action_doc in action_info['options']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
uncommon_options.append(action_doc)
|
|
||||||
|
|
||||||
action_info['options'] = uncommon_options
|
|
||||||
|
|
||||||
depth = 1 + get_actions(parser, action_info)
|
|
||||||
|
|
||||||
docs['actions'][action] = action_info
|
|
||||||
|
|
||||||
return depth
|
|
||||||
|
|
||||||
action_depth = get_actions(cli.parser, docs)
|
|
||||||
docs['content_depth'] = action_depth + 1
|
|
||||||
|
|
||||||
return docs
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateMan(Command):
|
|
||||||
name = 'generate-man'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def init_parser(cls, add_parser):
|
|
||||||
parser = add_parser(name=cls.name,
|
|
||||||
description='Generate cli documentation from cli docstrings')
|
|
||||||
|
|
||||||
parser.add_argument("-t", "--template-file", action="store", dest="template_file",
|
|
||||||
default=DEFAULT_TEMPLATE_FILE, help="path to jinja2 template")
|
|
||||||
parser.add_argument("-o", "--output-dir", action="store", dest="output_dir",
|
|
||||||
default='/tmp/', help="Output directory for rst files")
|
|
||||||
parser.add_argument("-f", "--output-format", action="store", dest="output_format",
|
|
||||||
default='man',
|
|
||||||
help="Output format for docs (the default 'man' or 'rst')")
|
|
||||||
parser.add_argument('cli_modules', help='CLI module name(s)', metavar='MODULE_NAME', nargs='*')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def main(args):
|
|
||||||
template_file = args.template_file
|
|
||||||
template_path = os.path.expanduser(template_file)
|
|
||||||
template_dir = os.path.abspath(os.path.dirname(template_path))
|
|
||||||
template_basename = os.path.basename(template_file)
|
|
||||||
|
|
||||||
output_dir = os.path.abspath(args.output_dir)
|
|
||||||
output_format = args.output_format
|
|
||||||
|
|
||||||
cli_modules = args.cli_modules
|
|
||||||
|
|
||||||
# various cli parsing things checks sys.argv if the 'args' that are passed in are []
|
|
||||||
# so just remove any args so the cli modules dont try to parse them resulting in warnings
|
|
||||||
sys.argv = [sys.argv[0]]
|
|
||||||
|
|
||||||
allvars = {}
|
|
||||||
output = {}
|
|
||||||
cli_list = []
|
|
||||||
cli_bin_name_list = []
|
|
||||||
|
|
||||||
# for binary in os.listdir('../../lib/ansible/cli'):
|
|
||||||
for cli_module_name in cli_modules:
|
|
||||||
binary = os.path.basename(os.path.expanduser(cli_module_name))
|
|
||||||
|
|
||||||
if not binary.endswith('.py'):
|
|
||||||
continue
|
|
||||||
elif binary == '__init__.py':
|
|
||||||
continue
|
|
||||||
|
|
||||||
cli_name = os.path.splitext(binary)[0]
|
|
||||||
|
|
||||||
if cli_name == 'adhoc':
|
|
||||||
cli_class_name = 'AdHocCLI'
|
|
||||||
# myclass = 'AdHocCLI'
|
|
||||||
output[cli_name] = 'ansible.1.rst.in'
|
|
||||||
cli_bin_name = 'ansible'
|
|
||||||
else:
|
|
||||||
# myclass = "%sCLI" % libname.capitalize()
|
|
||||||
cli_class_name = "%sCLI" % cli_name.capitalize()
|
|
||||||
output[cli_name] = 'ansible-%s.1.rst.in' % cli_name
|
|
||||||
cli_bin_name = 'ansible-%s' % cli_name
|
|
||||||
|
|
||||||
# FIXME:
|
|
||||||
allvars[cli_name] = opts_docs(cli_class_name, cli_name)
|
|
||||||
cli_bin_name_list.append(cli_bin_name)
|
|
||||||
|
|
||||||
cli_list = allvars.keys()
|
|
||||||
|
|
||||||
doc_name_formats = {'man': '%s.1.rst.in',
|
|
||||||
'rst': '%s.rst'}
|
|
||||||
|
|
||||||
for cli_name in cli_list:
|
|
||||||
|
|
||||||
# template it!
|
|
||||||
env = Environment(loader=FileSystemLoader(template_dir))
|
|
||||||
template = env.get_template(template_basename)
|
|
||||||
|
|
||||||
# add rest to vars
|
|
||||||
tvars = allvars[cli_name]
|
|
||||||
tvars['cli_bin_name_list'] = cli_bin_name_list
|
|
||||||
if '-i' in tvars['option_names']:
|
|
||||||
tvars['inventory'] = True
|
|
||||||
print('uses inventory')
|
|
||||||
if '-M' in tvars['option_names']:
|
|
||||||
tvars['library'] = True
|
|
||||||
print('uses library')
|
|
||||||
|
|
||||||
manpage = template.render(tvars)
|
|
||||||
filename = os.path.join(output_dir, doc_name_formats[output_format] % tvars['cli_name'])
|
|
||||||
update_file_if_different(filename, to_bytes(manpage))
|
|
||||||
Reference in New Issue
Block a user