ansible-doc 9.06 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
#!/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
27
import subprocess
28 29 30
from ansible import utils
from ansible.utils import module_docs
import ansible.constants as C
Jan-Piet Mens committed
31
from ansible.utils import version
32
import traceback
33 34 35

MODULEDIR = C.DEFAULT_MODULE_PATH

Jan-Piet Mens committed
36 37
BLACKLIST_EXTS = ('.swp', '.bak', '~', '.rpm')

38 39 40 41 42
_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\(([^)]+)\)")
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
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)
77 78 79 80 81 82 83 84 85 86 87

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

88
def get_man_text(doc):
89 90

    opt_indent="        "
91 92
    text = []
    text.append("> %s\n" % doc['module'].upper())
93 94 95

    desc = "".join(doc['description'])

96
    text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent="  ", subsequent_indent="  "))
97
    
Jan-Piet Mens committed
98
    if 'option_keys' in doc and len(doc['option_keys']) > 0:
99
        text.append("Options (= is mandatory):\n")
100 101 102 103 104 105 106 107 108

    for o in doc['option_keys']:
        opt = doc['options'][o]

        if opt.get('required', False):
            opt_leadin = "="
        else:
            opt_leadin = "-"

109
        text.append("%s %s" % (opt_leadin, o))
110

111 112 113
        desc = "".join(opt['description'])

        if 'choices' in opt:
Jan-Piet Mens committed
114
            choices = ", ".join(str(i) for i in opt['choices'])
115
            desc = desc + " (Choices: " + choices + ")"
116 117
        text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent,
                             subsequent_indent=opt_indent))
118

119
    if 'notes' in doc and len(doc['notes']) > 0:
120
        notes = "".join(doc['notes'])
121 122
        text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent="  ",
                            subsequent_indent=opt_indent))
123 124


125
    if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
126
        req = ", ".join(doc['requirements'])
127 128
        text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent="  ",
                            subsequent_indent=opt_indent))
129

130
    if 'examples' in doc and len(doc['examples']) > 0:
131
        text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
132
        for ex in doc['examples']:
133
            text.append("%s\n" % (ex['code']))
134

135
    if 'plainexamples' in doc and doc['plainexamples'] is not None:
136 137 138 139
        text.append(doc['plainexamples'])
    text.append('')

    return "\n".join(text)
140

141

142
def get_snippet_text(doc):
143

144
    text = []
145
    desc = tty_ify("".join(doc['short_description']))
146 147
    text.append("- name: %s" % (desc))
    text.append("  action: %s" % (doc['module']))
148 149 150 151 152

    for o in doc['options']:
        opt = doc['options'][o]
        desc = tty_ify("".join(opt['description']))
        s = o + "="
153 154 155 156 157 158 159 160 161 162 163 164 165
        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)
166 167 168

        if filename is None:
            continue
169 170
        if os.path.isdir(filename):
            continue
171

172 173 174 175 176 177 178 179 180 181 182
        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)
183 184 185 186

def main():

    p = optparse.OptionParser(
Jan-Piet Mens committed
187
        version=version("%prog"),
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
        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:
Jan-Piet Mens committed
212 213
        for i in options.module_path.split(os.pathsep):
            utils.plugins.module_finder.add_directory(i)
214 215 216 217 218 219 220 221 222

    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):
Jan-Piet Mens committed
223 224
                    if any(module.endswith(x) for x in BLACKLIST_EXTS):
                        continue
225 226
                    module_list.append(module)

227
        pager(get_module_list_text(module_list))
228 229 230 231
        sys.exit()

    if len(args) == 0:
        p.print_help()
232 233 234 235 236 237 238 239 240 241 242
   
    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)
 
243
    text = ''
244 245 246 247 248
    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,
249
                    print_paths(utils.plugins.module_finder)))
250 251
            continue

Jan-Piet Mens committed
252
        if any(filename.endswith(x) for x in BLACKLIST_EXTS):
253 254 255
            continue

        try:
256
            doc, plainexamples = module_docs.get_docstring(filename)
257
        except:
258 259
            traceback.print_exc()
            sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
260 261
            continue

262
        if doc is not None:
263 264 265 266 267 268 269 270 271 272

            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')
273
            doc['plainexamples']    = plainexamples
274 275

            if options.show_snippet:
276
                text += get_snippet_text(doc)
277
            else:
278
                text += get_man_text(doc)
279
        else:
280 281 282
            # 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)
283
    pager(text)
284 285 286

if __name__ == '__main__':
    main()