ansible-doc 10.6 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" ]
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 78 79 80 81
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)
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 145 146 147
        text.append(doc['plainexamples'])
    text.append('')

    return "\n".join(text)
148

149

150
def get_snippet_text(doc):
151

152
    text = []
153
    desc = tty_ify(" ".join(doc['short_description']))
154 155
    text.append("- name: %s" % (desc))
    text.append("  action: %s" % (doc['module']))
156

157
    for o in sorted(doc['options'].keys()):
158
        opt = doc['options'][o]
159
        desc = tty_ify(" ".join(opt['description']))
160 161 162 163 164 165

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

166 167 168 169 170 171
        text.append("      %-20s   # %s" % (s, desc))
    text.append('')

    return "\n".join(text)

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

        if module in module_docs.BLACKLIST_MODULES:
            continue

        filename = utils.plugins.module_finder.find_plugin(module)
187 188 189

        if filename is None:
            continue
190 191
        if filename.endswith(".ps1"):
            continue
192 193
        if os.path.isdir(filename):
            continue
194

195 196
        try:
            doc, plainexamples = module_docs.get_docstring(filename)
197 198 199 200
            desc = tty_ify(doc.get('short_description', '?')).strip()
            if len(desc) > linelimit:
                desc = desc[:linelimit] + '...'

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

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

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
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)

236 237 238
def main():

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

267
    if options.list_dir:
268
        # list modules
269 270 271
        paths = utils.plugins.module_finder._get_paths()
        module_list = []
        for path in paths:
272 273
            find_modules(path, module_list)

274
        pager(get_module_list_text(module_list))
275 276 277 278
        sys.exit()

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

280 281 282 283 284 285 286 287 288
    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)
289

290
    text = ''
291 292 293 294
    for module in args:

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

Jan-Piet Mens committed
298
        if any(filename.endswith(x) for x in BLACKLIST_EXTS):
299 300 301
            continue

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

308
        if doc is not None:
309 310 311 312 313 314 315 316 317 318

            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')
319
            doc['plainexamples']    = plainexamples
320 321

            if options.show_snippet:
322
                text += get_snippet_text(doc)
323
            else:
324
                text += get_man_text(doc)
325
        else:
326 327 328
            # 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)
329
    pager(text)
330 331 332

if __name__ == '__main__':
    main()