Add pager support to ansible-doc

If PAGER is set, or the executable less is present, ansible-doc will use
it to pipe information into so that it can be scrolled through.
If the environment variable LESS is not set, this will set it to FRSX.
This commit is contained in:
Stephen Fromm 2013-10-11 10:21:29 -07:00
parent a43ebf0b29
commit 79d6d344d8

View file

@ -24,6 +24,7 @@ import textwrap
import re import re
import optparse import optparse
import datetime import datetime
import subprocess
from ansible import utils from ansible import utils
from ansible.utils import module_docs from ansible.utils import module_docs
import ansible.constants as C import ansible.constants as C
@ -39,6 +40,40 @@ _BOLD = re.compile(r"B\(([^)]+)\)")
_MODULE = re.compile(r"M\(([^)]+)\)") _MODULE = re.compile(r"M\(([^)]+)\)")
_URL = re.compile(r"U\(([^)]+)\)") _URL = re.compile(r"U\(([^)]+)\)")
_CONST = re.compile(r"C\(([^)]+)\)") _CONST = re.compile(r"C\(([^)]+)\)")
PAGER = 'less'
LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars)
# -S (chop long lines) -X (disable termcap init and de-init)
def pager_print(text):
''' just print text '''
print text
def pager_pipe(text, cmd):
''' pipe text through a pager '''
if 'LESS' not in os.environ:
os.environ['LESS'] = LESS_OPTS
try:
cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
cmd.communicate(input=text)
except IOError:
pass
except KeyboardInterrupt:
pass
def pager(text):
''' find reasonable way to display text '''
# this is a much simpler form of what is in pydoc.py
if not sys.stdout.isatty():
pager_print(text)
elif 'PAGER' in os.environ:
if sys.platform == 'win32':
pager_print(text)
else:
pager_pipe(text, os.environ['PAGER'])
elif hasattr(os, 'system') and os.system('(less) 2> /dev/null') == 0:
pager_pipe(text, 'less')
else:
pager_print(text)
def tty_ify(text): def tty_ify(text):
@ -50,17 +85,18 @@ def tty_ify(text):
return t return t
def print_man(doc): def get_man_text(doc):
opt_indent=" " opt_indent=" "
print "> %s\n" % doc['module'].upper() text = []
text.append("> %s\n" % doc['module'].upper())
desc = "".join(doc['description']) desc = "".join(doc['description'])
print "%s\n" % textwrap.fill(tty_ify(desc), initial_indent=" ", subsequent_indent=" ") text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=" ", subsequent_indent=" "))
if 'option_keys' in doc and len(doc['option_keys']) > 0: if 'option_keys' in doc and len(doc['option_keys']) > 0:
print "Options (= is mandatory):\n" text.append("Options (= is mandatory):\n")
for o in doc['option_keys']: for o in doc['option_keys']:
opt = doc['options'][o] opt = doc['options'][o]
@ -70,47 +106,76 @@ def print_man(doc):
else: else:
opt_leadin = "-" opt_leadin = "-"
print "%s %s" % (opt_leadin, o) text.append("%s %s" % (opt_leadin, o))
desc = "".join(opt['description']) desc = "".join(opt['description'])
if 'choices' in opt: if 'choices' in opt:
choices = ", ".join(str(i) for i in opt['choices']) choices = ", ".join(str(i) for i in opt['choices'])
desc = desc + " (Choices: " + choices + ")" desc = desc + " (Choices: " + choices + ")"
print "%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent, text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent,
subsequent_indent=opt_indent) subsequent_indent=opt_indent))
if 'notes' in doc and len(doc['notes']) > 0: if 'notes' in doc and len(doc['notes']) > 0:
notes = "".join(doc['notes']) notes = "".join(doc['notes'])
print "Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent=" ", text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent=" ",
subsequent_indent=opt_indent) subsequent_indent=opt_indent))
if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0: if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
req = ", ".join(doc['requirements']) req = ", ".join(doc['requirements'])
print "Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent=" ", text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent=" ",
subsequent_indent=opt_indent) subsequent_indent=opt_indent))
if 'examples' in doc and len(doc['examples']) > 0: if 'examples' in doc and len(doc['examples']) > 0:
print "Example%s:\n" % ('' if len(doc['examples']) < 2 else 's') text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
for ex in doc['examples']: for ex in doc['examples']:
print "%s\n" % (ex['code']) text.append("%s\n" % (ex['code']))
if 'plainexamples' in doc and doc['plainexamples'] is not None: if 'plainexamples' in doc and doc['plainexamples'] is not None:
print doc['plainexamples'] text.append(doc['plainexamples'])
text.append('')
return "\n".join(text)
def print_snippet(doc): def get_snippet_text(doc):
text = []
desc = tty_ify("".join(doc['short_description'])) desc = tty_ify("".join(doc['short_description']))
print "- name: %s" % (desc) text.append("- name: %s" % (desc))
print " action: %s" % (doc['module']) text.append(" action: %s" % (doc['module']))
for o in doc['options']: for o in doc['options']:
opt = doc['options'][o] opt = doc['options'][o]
desc = tty_ify("".join(opt['description'])) desc = tty_ify("".join(opt['description']))
s = o + "=" s = o + "="
print " %-20s # %s" % (s, desc) text.append(" %-20s # %s" % (s, desc))
text.append('')
return "\n".join(text)
def get_module_list_text(module_list):
text = []
for module in sorted(set(module_list)):
if module in module_docs.BLACKLIST_MODULES:
continue
filename = utils.plugins.module_finder.find_plugin(module)
if os.path.isdir(filename):
continue
try:
doc, plainexamples = module_docs.get_docstring(filename)
desc = tty_ify(doc.get('short_description', '?'))
if len(desc) > 55:
desc = desc + '...'
text.append("%-20s %-60.60s" % (module, desc))
except:
traceback.print_exc()
sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
pass
return "\n".join(text)
def main(): def main():
@ -155,25 +220,7 @@ def main():
continue continue
module_list.append(module) module_list.append(module)
for module in sorted(set(module_list)): pager(get_module_list_text(module_list))
if module in module_docs.BLACKLIST_MODULES:
continue
filename = utils.plugins.module_finder.find_plugin(module)
if os.path.isdir(filename):
continue
try:
doc, plainexamples = module_docs.get_docstring(filename)
desc = tty_ify(doc.get('short_description', '?'))
if len(desc) > 55:
desc = desc + '...'
print "%-20s %-60.60s" % (module, desc)
except:
traceback.print_exc()
sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
pass
sys.exit() sys.exit()
if len(args) == 0: if len(args) == 0:
@ -189,6 +236,7 @@ def main():
ret.append(i) ret.append(i)
return os.pathsep.join(ret) return os.pathsep.join(ret)
text = ''
for module in args: for module in args:
filename = utils.plugins.module_finder.find_plugin(module) filename = utils.plugins.module_finder.find_plugin(module)
@ -221,13 +269,14 @@ def main():
doc['plainexamples'] = plainexamples doc['plainexamples'] = plainexamples
if options.show_snippet: if options.show_snippet:
print_snippet(doc) text += get_snippet_text(doc)
else: else:
print_man(doc) text += get_man_text(doc)
else: else:
# this typically means we couldn't even parse the docstring, not just that the YAML is busted, # this typically means we couldn't even parse the docstring, not just that the YAML is busted,
# probably a quoting issue. # probably a quoting issue.
sys.stderr.write("ERROR: module %s missing documentation (or could not parse documentation)\n" % module) sys.stderr.write("ERROR: module %s missing documentation (or could not parse documentation)\n" % module)
pager(text)
if __name__ == '__main__': if __name__ == '__main__':
main() main()