summaryrefslogtreecommitdiff
path: root/lib/ansible/cli/doc.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/cli/doc.py')
-rwxr-xr-xlib/ansible/cli/doc.py85
1 files changed, 75 insertions, 10 deletions
diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py
index 9f560bcb..4a5c8928 100755
--- a/lib/ansible/cli/doc.py
+++ b/lib/ansible/cli/doc.py
@@ -26,7 +26,7 @@ from ansible import context
from ansible.cli.arguments import option_helpers as opt_help
from ansible.collections.list import list_collection_dirs
from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError, AnsiblePluginNotFound
-from ansible.module_utils._text import to_native, to_text
+from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.common.json import json_dump
from ansible.module_utils.common.yaml import yaml_dump
@@ -163,8 +163,8 @@ class RoleMixin(object):
might be fully qualified with the collection name (e.g., community.general.roleA)
or not (e.g., roleA).
- :param collection_filter: A string containing the FQCN of a collection which will be
- used to limit results. This filter will take precedence over the name_filters.
+ :param collection_filter: A list of strings containing the FQCN of a collection which will
+ be used to limit results. This filter will take precedence over the name_filters.
:returns: A set of tuples consisting of: role name, collection name, collection path
"""
@@ -362,12 +362,23 @@ class DocCLI(CLI, RoleMixin):
_ITALIC = re.compile(r"\bI\(([^)]+)\)")
_BOLD = re.compile(r"\bB\(([^)]+)\)")
_MODULE = re.compile(r"\bM\(([^)]+)\)")
+ _PLUGIN = re.compile(r"\bP\(([^#)]+)#([a-z]+)\)")
_LINK = re.compile(r"\bL\(([^)]+), *([^)]+)\)")
_URL = re.compile(r"\bU\(([^)]+)\)")
_REF = re.compile(r"\bR\(([^)]+), *([^)]+)\)")
_CONST = re.compile(r"\bC\(([^)]+)\)")
+ _SEM_PARAMETER_STRING = r"\(((?:[^\\)]+|\\.)+)\)"
+ _SEM_OPTION_NAME = re.compile(r"\bO" + _SEM_PARAMETER_STRING)
+ _SEM_OPTION_VALUE = re.compile(r"\bV" + _SEM_PARAMETER_STRING)
+ _SEM_ENV_VARIABLE = re.compile(r"\bE" + _SEM_PARAMETER_STRING)
+ _SEM_RET_VALUE = re.compile(r"\bRV" + _SEM_PARAMETER_STRING)
_RULER = re.compile(r"\bHORIZONTALLINE\b")
+ # helper for unescaping
+ _UNESCAPE = re.compile(r"\\(.)")
+ _FQCN_TYPE_PREFIX_RE = re.compile(r'^([^.]+\.[^.]+\.[^#]+)#([a-z]+):(.*)$')
+ _IGNORE_MARKER = 'ignore:'
+
# rst specific
_RST_NOTE = re.compile(r".. note::")
_RST_SEEALSO = re.compile(r".. seealso::")
@@ -379,6 +390,40 @@ class DocCLI(CLI, RoleMixin):
super(DocCLI, self).__init__(args)
self.plugin_list = set()
+ @staticmethod
+ def _tty_ify_sem_simle(matcher):
+ text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1))
+ return f"`{text}'"
+
+ @staticmethod
+ def _tty_ify_sem_complex(matcher):
+ text = DocCLI._UNESCAPE.sub(r'\1', matcher.group(1))
+ value = None
+ if '=' in text:
+ text, value = text.split('=', 1)
+ m = DocCLI._FQCN_TYPE_PREFIX_RE.match(text)
+ if m:
+ plugin_fqcn = m.group(1)
+ plugin_type = m.group(2)
+ text = m.group(3)
+ elif text.startswith(DocCLI._IGNORE_MARKER):
+ text = text[len(DocCLI._IGNORE_MARKER):]
+ plugin_fqcn = plugin_type = ''
+ else:
+ plugin_fqcn = plugin_type = ''
+ entrypoint = None
+ if ':' in text:
+ entrypoint, text = text.split(':', 1)
+ if value is not None:
+ text = f"{text}={value}"
+ if plugin_fqcn and plugin_type:
+ plugin_suffix = '' if plugin_type in ('role', 'module', 'playbook') else ' plugin'
+ plugin = f"{plugin_type}{plugin_suffix} {plugin_fqcn}"
+ if plugin_type == 'role' and entrypoint is not None:
+ plugin = f"{plugin}, {entrypoint} entrypoint"
+ return f"`{text}' (of {plugin})"
+ return f"`{text}'"
+
@classmethod
def find_plugins(cls, path, internal, plugin_type, coll_filter=None):
display.deprecated("find_plugins method as it is incomplete/incorrect. use ansible.plugins.list functions instead.", version='2.17')
@@ -393,8 +438,13 @@ class DocCLI(CLI, RoleMixin):
t = cls._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word]
t = cls._URL.sub(r"\1", t) # U(word) => word
t = cls._LINK.sub(r"\1 <\2>", t) # L(word, url) => word <url>
+ t = cls._PLUGIN.sub("[" + r"\1" + "]", t) # P(word#type) => [word]
t = cls._REF.sub(r"\1", t) # R(word, sphinx-ref) => word
t = cls._CONST.sub(r"`\1'", t) # C(word) => `word'
+ t = cls._SEM_OPTION_NAME.sub(cls._tty_ify_sem_complex, t) # O(expr)
+ t = cls._SEM_OPTION_VALUE.sub(cls._tty_ify_sem_simle, t) # V(expr)
+ t = cls._SEM_ENV_VARIABLE.sub(cls._tty_ify_sem_simle, t) # E(expr)
+ t = cls._SEM_RET_VALUE.sub(cls._tty_ify_sem_complex, t) # RV(expr)
t = cls._RULER.sub("\n{0}\n".format("-" * 13), t) # HORIZONTALLINE => -------
# remove rst
@@ -495,7 +545,9 @@ class DocCLI(CLI, RoleMixin):
desc = desc[:linelimit] + '...'
pbreak = plugin.split('.')
- if pbreak[-1].startswith('_'): # Handle deprecated # TODO: add mark for deprecated collection plugins
+ # TODO: add mark for deprecated collection plugins
+ if pbreak[-1].startswith('_') and plugin.startswith(('ansible.builtin.', 'ansible.legacy.')):
+ # Handle deprecated ansible.builtin plugins
pbreak[-1] = pbreak[-1][1:]
plugin = '.'.join(pbreak)
deprecated.append("%-*s %-*.*s" % (displace, plugin, linelimit, len(desc), desc))
@@ -626,12 +678,11 @@ class DocCLI(CLI, RoleMixin):
def _get_collection_filter(self):
coll_filter = None
- if len(context.CLIARGS['args']) == 1:
- coll_filter = context.CLIARGS['args'][0]
- if not AnsibleCollectionRef.is_valid_collection_name(coll_filter):
- raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_filter))
- elif len(context.CLIARGS['args']) > 1:
- raise AnsibleOptionsError("Only a single collection filter is supported.")
+ if len(context.CLIARGS['args']) >= 1:
+ coll_filter = context.CLIARGS['args']
+ for coll_name in coll_filter:
+ if not AnsibleCollectionRef.is_valid_collection_name(coll_name):
+ raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_name))
return coll_filter
@@ -1251,6 +1302,20 @@ class DocCLI(CLI, RoleMixin):
relative_url = 'collections/%s_module.html' % item['module'].replace('.', '/', 2)
text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)),
limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent))
+ elif 'plugin' in item and 'plugin_type' in item:
+ plugin_suffix = ' plugin' if item['plugin_type'] not in ('module', 'role') else ''
+ text.append(textwrap.fill(DocCLI.tty_ify('%s%s %s' % (item['plugin_type'].title(), plugin_suffix, item['plugin'])),
+ limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))
+ description = item.get('description')
+ if description is None and item['plugin'].startswith('ansible.builtin.'):
+ description = 'The official documentation on the %s %s%s.' % (item['plugin'], item['plugin_type'], plugin_suffix)
+ if description is not None:
+ text.append(textwrap.fill(DocCLI.tty_ify(description),
+ limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent + ' '))
+ if item['plugin'].startswith('ansible.builtin.'):
+ relative_url = 'collections/%s_%s.html' % (item['plugin'].replace('.', '/', 2), item['plugin_type'])
+ text.append(textwrap.fill(DocCLI.tty_ify(get_versioned_doclink(relative_url)),
+ limit - 6, initial_indent=opt_indent + ' ', subsequent_indent=opt_indent))
elif 'name' in item and 'link' in item and 'description' in item:
text.append(textwrap.fill(DocCLI.tty_ify(item['name']),
limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))