bin/ansible-doc
17f31a2a
 #!/usr/bin/env python
 
 # (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
 #
 # This file is part of Ansible
 #
 # Ansible is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
 #
 # Ansible is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 #
 
 import os
 import sys
 import textwrap
 import re
 import optparse
 import datetime
79d6d344
 import subprocess
17f31a2a
 from ansible import utils
 from ansible.utils import module_docs
 import ansible.constants as C
77e06025
 from ansible.utils import version
36066d86
 import traceback
17f31a2a
 
 MODULEDIR = C.DEFAULT_MODULE_PATH
 
77e06025
 BLACKLIST_EXTS = ('.swp', '.bak', '~', '.rpm')
 
17f31a2a
 _ITALIC = re.compile(r"I\(([^)]+)\)")
 _BOLD   = re.compile(r"B\(([^)]+)\)")
 _MODULE = re.compile(r"M\(([^)]+)\)")
 _URL    = re.compile(r"U\(([^)]+)\)")
 _CONST  = re.compile(r"C\(([^)]+)\)")
79d6d344
 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)
17f31a2a
 
 def tty_ify(text):
 
     t = _ITALIC.sub("`" + r"\1" + "'", text)    # I(word) => `word'
     t = _BOLD.sub("*" + r"\1" + "*", t)         # B(word) => *word*
     t = _MODULE.sub("[" + r"\1" + "]", t)       # M(word) => [word]
     t = _URL.sub(r"\1", t)                      # U(word) => word
     t = _CONST.sub("`" + r"\1" + "'", t)        # C(word) => `word'
 
     return t
 
79d6d344
 def get_man_text(doc):
17f31a2a
 
     opt_indent="        "
79d6d344
     text = []
     text.append("> %s\n" % doc['module'].upper())
17f31a2a
 
     desc = "".join(doc['description'])
 
79d6d344
     text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent="  ", subsequent_indent="  "))
17f31a2a
     
77e06025
     if 'option_keys' in doc and len(doc['option_keys']) > 0:
79d6d344
         text.append("Options (= is mandatory):\n")
17f31a2a
 
30cabddf
     for o in sorted(doc['option_keys']):
17f31a2a
         opt = doc['options'][o]
 
         if opt.get('required', False):
             opt_leadin = "="
         else:
             opt_leadin = "-"
 
79d6d344
         text.append("%s %s" % (opt_leadin, o))
25f52d79
 
17f31a2a
         desc = "".join(opt['description'])
 
         if 'choices' in opt:
77e06025
             choices = ", ".join(str(i) for i in opt['choices'])
17f31a2a
             desc = desc + " (Choices: " + choices + ")"
37318ca0
         if 'default' in opt:
             default = str(opt['default'])
             desc = desc + " [Default: " + default + "]"
79d6d344
         text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent,
                              subsequent_indent=opt_indent))
17f31a2a
 
e913fb87
     if 'notes' in doc and len(doc['notes']) > 0:
17f31a2a
         notes = "".join(doc['notes'])
79d6d344
         text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent="  ",
                             subsequent_indent=opt_indent))
17f31a2a
 
 
d25888f4
     if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
eeb80c69
         req = ", ".join(doc['requirements'])
79d6d344
         text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent="  ",
                             subsequent_indent=opt_indent))
eeb80c69
 
52a55a35
     if 'examples' in doc and len(doc['examples']) > 0:
79d6d344
         text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
52a55a35
         for ex in doc['examples']:
79d6d344
             text.append("%s\n" % (ex['code']))
17f31a2a
 
396a07bc
     if 'plainexamples' in doc and doc['plainexamples'] is not None:
79d6d344
         text.append(doc['plainexamples'])
     text.append('')
 
     return "\n".join(text)
396a07bc
 
eeb80c69
 
79d6d344
 def get_snippet_text(doc):
17f31a2a
 
79d6d344
     text = []
17f31a2a
     desc = tty_ify("".join(doc['short_description']))
79d6d344
     text.append("- name: %s" % (desc))
     text.append("  action: %s" % (doc['module']))
17f31a2a
 
30cabddf
     for o in sorted(doc['options'].keys()):
17f31a2a
         opt = doc['options'][o]
         desc = tty_ify("".join(opt['description']))
e1b72782
 
         if opt.get('required', False):
             s = o + "="
         else:
             s = o
 
79d6d344
         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)
91e55693
 
         if filename is None:
             continue
5a2fd5c6
         if filename.endswith(".ps1"):
             continue
79d6d344
         if os.path.isdir(filename):
             continue
91e55693
 
79d6d344
         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)
     return "\n".join(text)
17f31a2a
 
 def main():
 
     p = optparse.OptionParser(
77e06025
         version=version("%prog"),
17f31a2a
         usage='usage: %prog [options] [module...]',
         description='Show Ansible module documentation',
     )
 
     p.add_option("-M", "--module-path",
             action="store",
             dest="module_path",
             default=MODULEDIR,
             help="Ansible modules/ directory")
     p.add_option("-l", "--list",
             action="store_true",
             default=False,
             dest='list_dir',
             help='List available modules')
     p.add_option("-s", "--snippet",
             action="store_true",
             default=False,
             dest='show_snippet',
             help='Show playbook snippet for specified module(s)')
     p.add_option('-v', action='version', help='Show version number and exit')
 
     (options, args) = p.parse_args()
 
     if options.module_path is not None:
77e06025
         for i in options.module_path.split(os.pathsep):
             utils.plugins.module_finder.add_directory(i)
17f31a2a
 
     if options.list_dir:
         # list all modules
         paths = utils.plugins.module_finder._get_paths()
         module_list = []
         for path in paths:
             # os.system("ls -C %s" % (path))
             if os.path.isdir(path):
                 for module in os.listdir(path):
77e06025
                     if any(module.endswith(x) for x in BLACKLIST_EXTS):
                         continue
17f31a2a
                     module_list.append(module)
 
79d6d344
         pager(get_module_list_text(module_list))
17f31a2a
         sys.exit()
 
     if len(args) == 0:
         p.print_help()
e6bf01a6
    
     def print_paths(finder):
         ''' Returns a string suitable for printing of the search path '''
 
         # Uses a list to get the order right
         ret = []
         for i in finder._get_paths():
             if i not in ret:
                 ret.append(i)
         return os.pathsep.join(ret)
  
79d6d344
     text = ''
17f31a2a
     for module in args:
 
         filename = utils.plugins.module_finder.find_plugin(module)
         if filename is None:
             sys.stderr.write("module %s not found in %s\n" % (module,
e6bf01a6
                     print_paths(utils.plugins.module_finder)))
17f31a2a
             continue
 
77e06025
         if any(filename.endswith(x) for x in BLACKLIST_EXTS):
17f31a2a
             continue
 
         try:
396a07bc
             doc, plainexamples = module_docs.get_docstring(filename)
17f31a2a
         except:
36066d86
             traceback.print_exc()
             sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
17f31a2a
             continue
 
36066d86
         if doc is not None:
17f31a2a
 
             all_keys = []
             for (k,v) in doc['options'].iteritems():
                 all_keys.append(k)
             all_keys = sorted(all_keys)
             doc['option_keys'] = all_keys
 
             doc['filename']         = filename
             doc['docuri']           = doc['module'].replace('_', '-')
             doc['now_date']         = datetime.date.today().strftime('%Y-%m-%d')
396a07bc
             doc['plainexamples']    = plainexamples
17f31a2a
 
             if options.show_snippet:
79d6d344
                 text += get_snippet_text(doc)
17f31a2a
             else:
79d6d344
                 text += get_man_text(doc)
17f31a2a
         else:
5c382487
             # this typically means we couldn't even parse the docstring, not just that the YAML is busted,
             # probably a quoting issue.
             sys.stderr.write("ERROR: module %s missing documentation (or could not parse documentation)\n" % module)
79d6d344
     pager(text)
17f31a2a
 
 if __name__ == '__main__':
     main()