diff options
Diffstat (limited to 'bin/ansible-doc')
-rwxr-xr-x | bin/ansible-doc | 85 |
1 files changed, 75 insertions, 10 deletions
diff --git a/bin/ansible-doc b/bin/ansible-doc index 9f560bcb..4a5c8928 100755 --- a/bin/ansible-doc +++ b/bin/ansible-doc @@ -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)) |