ansible-doc 10.8 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 31
import fcntl
import termios
import struct

32 33 34
from ansible import utils
from ansible.utils import module_docs
import ansible.constants as C
Jan-Piet Mens committed
35
from ansible.utils import version
36
import traceback
37 38 39

MODULEDIR = C.DEFAULT_MODULE_PATH

40
BLACKLIST_EXTS = ('.pyc', '.swp', '.bak', '~', '.rpm')
41
IGNORE_FILES = [ "COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION"]
Jan-Piet Mens committed
42

43 44 45 46 47
_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\(([^)]+)\)")
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 77
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'])
78
    elif subprocess.call('(less --version) 2> /dev/null', shell = True) == 0:
79 80 81
        pager_pipe(text, 'less')
    else:
        pager_print(text)
82 83 84 85 86 87 88 89 90 91 92

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

93
def get_man_text(doc):
94 95

    opt_indent="        "
96 97
    text = []
    text.append("> %s\n" % doc['module'].upper())
98

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

101
    text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent="  ", subsequent_indent="  "))
102

Jan-Piet Mens committed
103
    if 'option_keys' in doc and len(doc['option_keys']) > 0:
104
        text.append("Options (= is mandatory):\n")
105

106
    for o in sorted(doc['option_keys']):
107 108 109 110 111 112 113
        opt = doc['options'][o]

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

114
        text.append("%s %s" % (opt_leadin, o))
115

116
        desc = " ".join(opt['description'])
117 118

        if 'choices' in opt:
Jan-Piet Mens committed
119
            choices = ", ".join(str(i) for i in opt['choices'])
120
            desc = desc + " (Choices: " + choices + ")"
121 122 123
        if 'default' in opt:
            default = str(opt['default'])
            desc = desc + " [Default: " + default + "]"
124 125
        text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent,
                             subsequent_indent=opt_indent))
126

127
    if 'notes' in doc and len(doc['notes']) > 0:
128
        notes = " ".join(doc['notes'])
129 130
        text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent="  ",
                            subsequent_indent=opt_indent))
131 132


133
    if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
134
        req = ", ".join(doc['requirements'])
135 136
        text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent="  ",
                            subsequent_indent=opt_indent))
137

138
    if 'examples' in doc and len(doc['examples']) > 0:
139
        text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
140
        for ex in doc['examples']:
141
            text.append("%s\n" % (ex['code']))
142

143
    if 'plainexamples' in doc and doc['plainexamples'] is not None:
144
        text.append("EXAMPLES:")
145
        text.append(doc['plainexamples'])
146 147 148
    if 'returndocs' in doc and doc['returndocs'] is not None:
        text.append("RETURN VALUES:")
        text.append(doc['returndocs'])
149 150 151
    text.append('')

    return "\n".join(text)
152

153

154
def get_snippet_text(doc):
155

156
    text = []
157
    desc = tty_ify(" ".join(doc['short_description']))
158 159
    text.append("- name: %s" % (desc))
    text.append("  action: %s" % (doc['module']))
160

161
    for o in sorted(doc['options'].keys()):
162
        opt = doc['options'][o]
163
        desc = tty_ify(" ".join(opt['description']))
164 165 166 167 168 169

        if opt.get('required', False):
            s = o + "="
        else:
            s = o

170 171 172 173 174 175
        text.append("      %-20s   # %s" % (s, desc))
    text.append('')

    return "\n".join(text)

def get_module_list_text(module_list):
176 177
    tty_size = 0
    if os.isatty(0):
178 179
        tty_size = struct.unpack('HHHH',
            fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[1]
180
    columns = max(60, tty_size)
181 182
    displace = max(len(x) for x in module_list)
    linelimit = columns - displace - 5
183
    text = []
184
    deprecated = []
185 186 187 188 189 190
    for module in sorted(set(module_list)):

        if module in module_docs.BLACKLIST_MODULES:
            continue

        filename = utils.plugins.module_finder.find_plugin(module)
191 192 193

        if filename is None:
            continue
194 195
        if filename.endswith(".ps1"):
            continue
196 197
        if os.path.isdir(filename):
            continue
198

199 200
        try:
            doc, plainexamples = module_docs.get_docstring(filename)
201 202 203 204
            desc = tty_ify(doc.get('short_description', '?')).strip()
            if len(desc) > linelimit:
                desc = desc[:linelimit] + '...'

Brian Coca committed
205
            if module.startswith('_'): # Handle deprecated
206
                deprecated.append("%-*s %-*.*s" % (displace, module[1:], linelimit, len(desc), desc))
207 208
            else:
                text.append("%-*s %-*.*s" % (displace, module, linelimit, len(desc), desc))
209 210 211
        except:
            traceback.print_exc()
            sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
212 213 214 215

    if len(deprecated) > 0:
        text.append("\nDEPRECATED:")
        text.extend(deprecated)
216
    return "\n".join(text)
217

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
def find_modules(path, module_list):

    if os.path.isdir(path):
        for module in os.listdir(path):
            if module.startswith('.'):
                continue
            elif os.path.isdir(module):
                find_modules(module, module_list)
            elif any(module.endswith(x) for x in BLACKLIST_EXTS):
                continue
            elif module.startswith('__'):
                continue
            elif module in IGNORE_FILES:
                continue
            elif module.startswith('_'):
                fullpath = '/'.join([path,module])
                if os.path.islink(fullpath): # avoids aliases
                    continue

            module = os.path.splitext(module)[0] # removes the extension
            module_list.append(module)

240 241 242
def main():

    p = optparse.OptionParser(
Jan-Piet Mens committed
243
        version=version("%prog"),
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
        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
268 269
        for i in options.module_path.split(os.pathsep):
            utils.plugins.module_finder.add_directory(i)
270

271
    if options.list_dir:
272
        # list modules
273 274 275
        paths = utils.plugins.module_finder._get_paths()
        module_list = []
        for path in paths:
276 277
            find_modules(path, module_list)

278
        pager(get_module_list_text(module_list))
279 280 281 282
        sys.exit()

    if len(args) == 0:
        p.print_help()
283

284 285 286 287 288 289 290 291 292
    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)
293

294
    text = ''
295 296 297 298
    for module in args:

        filename = utils.plugins.module_finder.find_plugin(module)
        if filename is None:
299
            sys.stderr.write("module %s not found in %s\n" % (module, print_paths(utils.plugins.module_finder)))
300 301
            continue

Jan-Piet Mens committed
302
        if any(filename.endswith(x) for x in BLACKLIST_EXTS):
303 304 305
            continue

        try:
306
            doc, plainexamples, returndocs = module_docs.get_docstring(filename)
307
        except:
308 309
            traceback.print_exc()
            sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
310 311
            continue

312
        if doc is not None:
313 314 315 316 317 318 319 320 321 322

            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')
323
            doc['plainexamples']    = plainexamples
324
            doc['returndocs']       = returndocs
325 326

            if options.show_snippet:
327
                text += get_snippet_text(doc)
328
            else:
329
                text += get_man_text(doc)
330
        else:
331 332 333
            # 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)
334
    pager(text)
335 336 337

if __name__ == '__main__':
    main()