diff --git a/docs/docsite/Makefile b/docs/docsite/Makefile index 8a3723ee3d..20d2cb86e1 100644 --- a/docs/docsite/Makefile +++ b/docs/docsite/Makefile @@ -2,7 +2,7 @@ OS := $(shell uname -s) PLUGIN_FORMATTER=../../hacking/build-ansible.py docs-build KEYWORD_DUMPER=../../hacking/build-ansible.py document-keywords 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 ifeq ($(shell echo $(OS) | grep -Eic 'Darwin|FreeBSD|OpenBSD|DragonFly'),1) CPUS ?= $(shell sysctl hw.ncpu|awk '{print $$2}') @@ -194,10 +194,8 @@ clean: 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 -# TODO: make generate_man output dir cli option cli: - mkdir -p 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 + $(GENERATE_CLI) --output-dir=rst/cli/ 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) diff --git a/docs/templates/cli_rst.j2 b/docs/templates/cli_rst.j2 deleted file mode 100644 index 5257a581c7..0000000000 --- a/docs/templates/cli_rst.j2 +++ /dev/null @@ -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 %} diff --git a/hacking/build_library/build_ansible/command_plugins/generate_man.py b/hacking/build_library/build_ansible/command_plugins/generate_man.py deleted file mode 100644 index d241838dca..0000000000 --- a/hacking/build_library/build_ansible/command_plugins/generate_man.py +++ /dev/null @@ -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._text 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))