mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-25 11:51:26 -07:00
generate rst doc pages for command line tools (#27530)
* let generate_man also gen rst pages for cli tools * make template-file, output-dir, output format cli options for generate_man * update main Makefile to use generate_man.py for docs (man pages and rst) * update vault docs that use :option: * Edits based on6e34ea6242
anda3afc78535
* add a optparse 'desc' to lib/ansible/cli/config.py The man page needs a short desc for the 'NAME' field which it gets from the option parse 'desc' value. Fixes building ansible-config man page. * add trim_docstring from pep257 to generate_man use pep258 docstring trim function to fix up any indention weirdness inherit to doc strings (ie, lines other than first line being indented. * Add refs to cli command actions To reference ansible-vaults --vault-id option, use: :option:`The link text here <ansible-vault --vault-id>` or: :option:`--vault-id <ansible-vault --vault-id>` To reference ansible-vault's 'encrypt' action, use: :ref:`The link text here <ansible_vault_encrypt>` or most of the time: :ref:`ansible-vault encrypt <ansible_vault_encrypt>`
This commit is contained in:
parent
4a73390823
commit
89c973445c
9 changed files with 404 additions and 82 deletions
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
@ -8,6 +10,46 @@ from jinja2 import Environment, FileSystemLoader
|
|||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
|
||||
def generate_parser():
|
||||
p = optparse.OptionParser(
|
||||
version='%prog 1.0',
|
||||
usage='usage: %prog [options]',
|
||||
description='Generate cli documentation from cli docstrings',
|
||||
)
|
||||
|
||||
p.add_option("-t", "--template-file", action="store", dest="template_file", default="../templates/man.j2", help="path to jinja2 template")
|
||||
p.add_option("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/', help="Output directory for rst files")
|
||||
p.add_option("-f", "--output-format", action="store", dest="output_format", default='man', help="Output format for docs (the default 'man' or 'rst')")
|
||||
return p
|
||||
|
||||
|
||||
# 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.maxint
|
||||
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.maxint:
|
||||
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 '''
|
||||
|
||||
|
@ -24,107 +66,215 @@ def get_options(optlist):
|
|||
return opts
|
||||
|
||||
|
||||
def get_option_groups(option_parser):
|
||||
groups = []
|
||||
for option_group in option_parser.option_groups:
|
||||
group_info = {}
|
||||
group_info['desc'] = option_group.get_description()
|
||||
group_info['options'] = option_group.option_list
|
||||
group_info['group_obj'] = option_group
|
||||
groups.append(group_info)
|
||||
return groups
|
||||
|
||||
|
||||
def opt_doc_list(cli):
|
||||
''' iterate over options lists '''
|
||||
|
||||
results = []
|
||||
for optg in cli.parser.option_groups:
|
||||
results.extend(get_options(optg.option_list))
|
||||
for option_group in cli.parser.option_groups:
|
||||
results.extend(get_options(option_group.option_list))
|
||||
|
||||
results.extend(get_options(cli.parser.option_list))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def opts_docs(cli, name):
|
||||
# def opts_docs(cli, name):
|
||||
def opts_docs(cli_class_name, cli_module_name):
|
||||
''' generate doc structure from options '''
|
||||
|
||||
# cli name
|
||||
if '-' in name:
|
||||
name = name.split('-')[1]
|
||||
else:
|
||||
name = 'adhoc'
|
||||
cli_name = 'ansible-%s' % cli_module_name
|
||||
if cli_module_name == 'adhoc':
|
||||
cli_name = 'ansible'
|
||||
|
||||
# cli info
|
||||
# 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([])
|
||||
|
||||
# parse the common options
|
||||
try:
|
||||
cli.parse()
|
||||
except:
|
||||
pass
|
||||
|
||||
# base/common cli info
|
||||
docs = {
|
||||
'cli': name,
|
||||
'cli': cli_module_name,
|
||||
'cli_name': cli_name,
|
||||
'usage': cli.parser.usage,
|
||||
'short_desc': cli.parser.description,
|
||||
'long_desc': cli.__doc__,
|
||||
'long_desc': trim_docstring(cli.__doc__),
|
||||
'actions': {},
|
||||
}
|
||||
option_info = {'option_names': [],
|
||||
'options': [],
|
||||
'groups': []}
|
||||
|
||||
for extras in ('ARGUMENTS'):
|
||||
if hasattr(cli, extras):
|
||||
docs[extras.lower()] = getattr(cli, extras)
|
||||
|
||||
common_opts = opt_doc_list(cli)
|
||||
groups_info = get_option_groups(cli.parser)
|
||||
shared_opt_names = []
|
||||
for opt in common_opts:
|
||||
shared_opt_names.extend(opt.get('options', []))
|
||||
|
||||
option_info['options'] = common_opts
|
||||
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
|
||||
if cli.VALID_ACTIONS:
|
||||
docs['actions'] = {}
|
||||
|
||||
# use class attrs not the attrs on a instance (not that it matters here...)
|
||||
for action in getattr(cli_klass, 'VALID_ACTIONS', ()):
|
||||
# instantiate each cli and ask its options
|
||||
action_cli_klass = getattr(__import__("ansible.cli.%s" % cli_module_name,
|
||||
fromlist=[cli_class_name]), cli_class_name)
|
||||
# init with args with action added?
|
||||
cli = action_cli_klass([])
|
||||
cli.args.append(action)
|
||||
|
||||
try:
|
||||
cli.parse()
|
||||
except:
|
||||
pass
|
||||
|
||||
# FIXME/TODO: needed?
|
||||
# avoid dupe errors
|
||||
cli.parser.set_conflict_handler('resolve')
|
||||
for action in cli.VALID_ACTIONS:
|
||||
cli.args.append(action)
|
||||
cli.set_action()
|
||||
docs['actions'][action] = getattr(cli, 'execute_%s' % action).__doc__
|
||||
|
||||
cli.set_action()
|
||||
|
||||
action_info = {'option_names': [],
|
||||
'options': []}
|
||||
# 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(cli)
|
||||
|
||||
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
|
||||
|
||||
docs['actions'][action] = action_info
|
||||
|
||||
docs['options'] = opt_doc_list(cli)
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
template_file = 'man.j2'
|
||||
parser = generate_parser()
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
template_file = options.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(options.output_dir)
|
||||
output_format = options.output_format
|
||||
|
||||
cli_modules = args
|
||||
|
||||
# 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]]
|
||||
# need to be in right dir
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
|
||||
allvars = {}
|
||||
output = {}
|
||||
cli_list = []
|
||||
for binary in os.listdir('../../lib/ansible/cli'):
|
||||
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
|
||||
|
||||
libname = os.path.splitext(binary)[0]
|
||||
print("Found CLI %s" % libname)
|
||||
cli_name = os.path.splitext(binary)[0]
|
||||
|
||||
if libname == 'adhoc':
|
||||
myclass = 'AdHocCLI'
|
||||
output[libname] = 'ansible.1.asciidoc.in'
|
||||
if cli_name == 'adhoc':
|
||||
cli_class_name = 'AdHocCLI'
|
||||
# myclass = 'AdHocCLI'
|
||||
output[cli_name] = 'ansible.1.asciidoc.in'
|
||||
cli_bin_name = 'ansible'
|
||||
else:
|
||||
myclass = "%sCLI" % libname.capitalize()
|
||||
output[libname] = 'ansible-%s.1.asciidoc.in' % libname
|
||||
# myclass = "%sCLI" % libname.capitalize()
|
||||
cli_class_name = "%sCLI" % cli_name.capitalize()
|
||||
output[cli_name] = 'ansible-%s.1.asciidoc.in' % cli_name
|
||||
cli_bin_name = 'ansible-%s' % cli_name
|
||||
|
||||
# instantiate each cli and ask its options
|
||||
mycli = getattr(__import__("ansible.cli.%s" % libname, fromlist=[myclass]), myclass)
|
||||
cli_object = mycli([])
|
||||
try:
|
||||
cli_object.parse()
|
||||
except:
|
||||
# no options passed, we expect errors
|
||||
pass
|
||||
|
||||
allvars[libname] = opts_docs(cli_object, libname)
|
||||
|
||||
for extras in ('ARGUMENTS'):
|
||||
if hasattr(cli_object, extras):
|
||||
allvars[libname][extras.lower()] = getattr(cli_object, extras)
|
||||
# FIXME:
|
||||
allvars[cli_name] = opts_docs(cli_class_name, cli_name)
|
||||
cli_bin_name_list.append(cli_bin_name)
|
||||
|
||||
cli_list = allvars.keys()
|
||||
for libname in cli_list:
|
||||
|
||||
doc_name_formats = {'man': '%s.1.asciidoc.in',
|
||||
'rst': '%s.rst'}
|
||||
|
||||
for cli_name in cli_list:
|
||||
|
||||
# template it!
|
||||
env = Environment(loader=FileSystemLoader('../templates'))
|
||||
template = env.get_template('man.j2')
|
||||
env = Environment(loader=FileSystemLoader(template_dir))
|
||||
template = env.get_template(template_basename)
|
||||
|
||||
# add rest to vars
|
||||
tvars = allvars[libname]
|
||||
tvars = allvars[cli_name]
|
||||
tvars['cli_list'] = cli_list
|
||||
tvars['cli'] = libname
|
||||
tvars['cli_bin_name_list'] = cli_bin_name_list
|
||||
tvars['cli'] = cli_name
|
||||
if '-i' in tvars['options']:
|
||||
print('uses inventory')
|
||||
|
||||
manpage = template.render(tvars)
|
||||
filename = '../man/man1/%s' % output[libname]
|
||||
filename = os.path.join(output_dir, doc_name_formats[output_format] % tvars['cli_name'])
|
||||
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(to_bytes(manpage))
|
||||
print("Wrote man docs to %s" % filename)
|
||||
print("Wrote doc to %s" % filename)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue