From d47e15a1f222d059703582da200fc5172ffde973 Mon Sep 17 00:00:00 2001 From: Jan-Piet Mens Date: Wed, 26 Sep 2012 20:41:44 +0200 Subject: [PATCH] module_formatter in hacking/ --- Makefile | 2 + docs/man/man1/ansible.lineinfile.man | 57 +++++ hacking/module_formatter.py | 297 +++++++++++++++++++++++++++ hacking/templates/html.j2 | 7 + hacking/templates/latex.j2 | 70 +++++++ hacking/templates/man.j2 | 64 ++++++ hacking/templates/rst.j2 | 33 +++ 7 files changed, 530 insertions(+) create mode 100644 docs/man/man1/ansible.lineinfile.man create mode 100755 hacking/module_formatter.py create mode 100644 hacking/templates/html.j2 create mode 100644 hacking/templates/latex.j2 create mode 100644 hacking/templates/man.j2 create mode 100644 hacking/templates/rst.j2 diff --git a/Makefile b/Makefile index 80faa51342d..90fc8fe9834 100644 --- a/Makefile +++ b/Makefile @@ -160,3 +160,5 @@ deb: debian # for arch or gentoo, read instructions in the appropriate 'packaging' subdirectory directory +manpages: + hacking/module_formatter.py -t man -o docs/man/man1/ --module-dir=library --template-dir=hacking/templates diff --git a/docs/man/man1/ansible.lineinfile.man b/docs/man/man1/ansible.lineinfile.man new file mode 100644 index 00000000000..7b9c12eaae0 --- /dev/null +++ b/docs/man/man1/ansible.lineinfile.man @@ -0,0 +1,57 @@ +.TH ANSIBLE.LINEINFILE 5 "2012-09-26" "unknown" "ANSIBLE MODULES" +." generated from library/lineinfile +.SH NAME +lineinfile \- Ensure a particular line is in a file +." ------ DESCRIPTION +.SH DESCRIPTION +.PP +This module will search a file for a line, and ensure that it is present or absent. +.PP +This is primarily useful when you want to change a single line in a file only. For other cases, see the \fIcopy\fR or \fItemplate\fR modules. + +." ------ OPTIONS +." +." +.SH OPTIONS +.IP name +The file to modify +(required) +.IP regexp +The regular expression to look for in the file. For \fIstate=present\fR, the pattern to replace. For \fIstate=absent\fR, the pattern of the line to remove. +(required) +.IP state +Whether the line should be there or not. +." .SS Choices +.IR Choices : +present,absent. +.IP line +Required for \fIstate=present\fR. The line to insert/replace into the file. Must match the value given to \fCregexp\fR. + +.IP insertafter +Used with \fIstate=present\fR. If specified, the line will be inserted after the specified regular expression. Two special values are available; \fCBOF\fR for inserting the line at the beginning of the file, and \fCEOF\fR for inserting the line at the end of the file. +." .SS Choices +.IR Choices : +BOF,EOF. +.IP backup +Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. + + +." +." +." ------ NOTES +." +." +." ------ EXAMPLES +.SH EXAMPLES +.PP +.nf +lineinfile name=/etc/selinux/config regexp=^SELINUX= line=SELINUX=disabled +.fi +.PP +.nf +lineinfile name=/etc/sudoers state=absent regexp="^%wheel" +.fi +." ------- AUTHOR +.SH SEE ALSO +.IR ansible (1), +.I http://ansible.github.com/modules.html#lineinfile \ No newline at end of file diff --git a/hacking/module_formatter.py b/hacking/module_formatter.py new file mode 100755 index 00000000000..7fad738e3a6 --- /dev/null +++ b/hacking/module_formatter.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python +# (c) 2012, Jan-Piet Mens +# +# 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 . +# + +import os +import sys +import yaml +import codecs +import json +import ast +from jinja2 import Environment, FileSystemLoader +import re +import argparse +import time +import datetime +import subprocess + +MODULEDIR="/Users/jpm/Auto/pubgit/ansible/ansible/library" + +BOILERPLATE = ''' +--- +module: foo +author: AUTHORNAME +short_description: A short description, think title +description: + - First paragraph explains what the module does. More paragraphs can + be added. + - Second para of description. You can use B(bold), I(italic), and + C(constant-width). To refer to another M(module) use that, and + U(url) exists too. +version_added: "0.x" +options: + - dest: + required: true + description: + - What does this option do, and bla bla bla + - More than one paragraph allowed here as well. Formatting + with B(bold), etc. work too. + - remove: + required: false + choices: [ yes, no, maybe, perhaps ] + default: "maybe" + aliases: [ kill, killme, delete ] + description: + - The foo to do on M(module) but be careful of lorem ipsum +examples: + - code: foo dest=/tmp/jj remove=maybe + description: Possibly removes the specified file + - code: foo dest=/dev/null +''' + +# There is a better way of doing this! +# TODO: somebody add U(text, http://foo.bar/) as described by Tim in #991 + +_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\(([^)]+)\)") + +def latex_ify(text): + + t = _ITALIC.sub("\\I{" + r"\1" + "}", text) + t = _BOLD.sub("\\B{" + r"\1" + "}", t) + t = _MODULE.sub("\\M{" + r"\1" + "}", t) + t = _URL.sub("\\url{" + r"\1" + "}", t) + t = _CONST.sub("\\C{" + r"\1" + "}", t) + + return t + +def html_ify(text): + + t = _ITALIC.sub("" + r"\1" + "", text) + t = _BOLD.sub("" + r"\1" + "", t) + t = _MODULE.sub("" + r"\1" + "", t) + t = _URL.sub("" + r"\1" + "", t) + t = _CONST.sub("" + r"\1" + "", t) + + return t + +def man_ify(text): + + t = _ITALIC.sub(r'\\fI' + r"\1" + r"\\fR", text) + t = _BOLD.sub(r'\\fB' + r"\1" + r"\\fR", t) + t = _MODULE.sub(r'\\fI' + r"\1" + r"\\fR", t) + t = _URL.sub(r'\\fI' + r"\1" + r"\\fR", t) + t = _CONST.sub(r'\\fC' + r"\1" + r"\\fR", t) + + return t + +def rst_ify(text): + + t = _ITALIC.sub(r'*' + r"\1" + r"*", text) + t = _BOLD.sub(r'**' + r"\1" + r"**", t) + t = _MODULE.sub(r'``' + r"\1" + r"``", t) + t = _URL.sub(r"\1", t) + t = _CONST.sub(r'``' + r"\1" + r"``", t) + + return t + +# Helper for Jinja2 (format() doesn't work here...) +def rst_fmt(text, fmt): + return fmt % (text) + +def rst_xline(width, char="="): + return char * width + + +env = Environment(loader=FileSystemLoader('templates'), + variable_start_string="@{", + variable_end_string="}@", + ) + +env.globals['xline'] = rst_xline + + +def get_docstring(filename, verbose=False): + """ + Search for assignment of the DOCUMENTATION variable in the given file. + Parse that from YAML and return the YAML doc or None. + """ + + doc = None + + try: + # Thank you, Habbie, for this bit of code :-) + M = ast.parse(''.join(open(filename))) + for child in M.body: + if isinstance(child, ast.Assign): + if 'DOCUMENTATION' in (t.id for t in child.targets): + doc = yaml.load(child.value.s) + except: + if verbose: + raise + pass + return doc + +def main(): + + p = argparse.ArgumentParser(description="Convert Ansible module DOCUMENTATION strings to other formats") + + p.add_argument("-A", "--ansible-version", + action="store", + dest="ansible_version", + default="unknown", + help="Ansible version number") + p.add_argument("-M", "--module-dir", + action="store", + dest="module_dir", + default=MODULEDIR, + help="Ansible modules/ directory") + p.add_argument("-t", "--type", + action='store', + dest='type', + choices=['html', 'latex', 'man', 'rst'], + default='latex', + help="Output type") + p.add_argument("-m", "--module", + action='append', + default=[], + dest='module_list', + help="Add modules to process in module_dir") + p.add_argument("-v", "--verbose", + action='store_true', + default=False, + help="Verbose") + p.add_argument("-o", "--output-dir", + action="store", + dest="output_dir", + default=None, + help="Output directory for module files") + p.add_argument("-G", "--generate", + action="store_true", + dest="do_boilerplate", + default=False, + help="generate boilerplate DOCUMENTATION to stdout") + p.add_argument('-V', '--version', action='version', version='%(prog)s 1.0') + + module_dir = None + args = p.parse_args() + + # print "M: %s" % args.module_dir + # print "t: %s" % args.type + # print "m: %s" % args.module_list + # print "v: %s" % args.verbose + + if args.do_boilerplate: + boilerplate() + sys.exit(0) + + if not args.module_dir: + print "Need module_dir" + sys.exit(1) + + if args.type == 'latex': + env.filters['jpfunc'] = latex_ify + template = env.get_template('latex.j2') + outputname = "%s.tex" + if args.type == 'html': + env.filters['jpfunc'] = html_ify + template = env.get_template('html.j2') + outputname = "%s.html" + if args.type == 'man': + env.filters['jpfunc'] = man_ify + template = env.get_template('man.j2') + outputname = "ansible.%s.man" + if args.type == 'rst': + env.filters['jpfunc'] = rst_ify + env.filters['fmt'] = rst_fmt + env.filters['xline'] = rst_xline + template = env.get_template('rst.j2') + outputname = "%s.rst" + + for module in os.listdir(args.module_dir): + if len(args.module_list): + if not module in args.module_list: + continue + + fname = os.path.join(args.module_dir, module) + extra = os.path.join("inc", "%s.tex" % module) + + # FIXME: html/manpage/latex + print "%% modules2.py ---> %s" % fname + + doc = get_docstring(fname, verbose=args.verbose) + if not doc is None: + + doc['filename'] = fname + doc['docuri'] = doc['module'].replace('_', '-') + doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d') + doc['ansible_version'] = args.ansible_version + + if args.verbose: + print json.dumps(doc, indent=4) + + + if args.type == 'latex': + if os.path.exists(extra): + f = open(extra) + extradata = f.read() + f.close() + doc['extradata'] = extradata + + text = template.render(doc) + if args.output_dir is not None: + f = open(os.path.join(args.output_dir, outputname % module), 'w') + f.write(text) + f.close() + else: + print text + +def boilerplate(): + + # Sneaky: insert author's name from Git config + + cmd = subprocess.Popen("git config --get user.name", shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = cmd.communicate() + + if len(out.split('\n')) == 2: + author = out.split('\n')[0] + print author + else: + author = "Your Name" + + # I can't dump YAML in ordered fasion, so I use this boilerplate string + # and verify it is parseable just before printing it out to the user. + + try: + boilplate = yaml.load(BOILERPLATE) + except: + print "Something is wrong with the BOILERPLATE" + sys.exit(1) + + print """ +DOCUMENTATION = ''' +%s +''' +"""[1:-1] % (BOILERPLATE.replace('AUTHORNAME', author) [1:-1] ) + +if __name__ == '__main__': + main() diff --git a/hacking/templates/html.j2 b/hacking/templates/html.j2 new file mode 100644 index 00000000000..b86e47378b8 --- /dev/null +++ b/hacking/templates/html.j2 @@ -0,0 +1,7 @@ + + +

@{module}@

+ + {% for desc in description -%} + @{ desc | jpfunc }@ + {% endfor %} diff --git a/hacking/templates/latex.j2 b/hacking/templates/latex.j2 new file mode 100644 index 00000000000..0ae8057926b --- /dev/null +++ b/hacking/templates/latex.j2 @@ -0,0 +1,70 @@ +%--- @{ module | upper }@ ---- from @{ filename }@ --- + +%: -- module header +\mods{@{module}@}{@{docuri}@}{ + {% for desc in description -%} + @{ desc | jpfunc }@ + {% endfor -%} + {% if version_added is defined -%} + (\I{* new in version @{ version_added }@}) + {% endif -%} + } + +%: -- module options + +{% if options is defined -%} +\begin{xlist}{abcdefghijklmno} + {% for o in options -%} + {% for opt, v in o.iteritems() -%} + + {% if v['required'] -%} + \item[\man\,\C{@{ opt }@}] + {% else -%} + \item[\opt\,\C{@{ opt }@}] + {% endif -%} + + {# -------- option description ----------#} + {% for desc in v.description -%} + @{ desc | jpfunc }@ + {% endfor %} + {% if v['choices'] -%} + \B{Choices}:\, + {% for choice in v['choices'] -%}\C{@{ choice }@}{% if not loop.last %},{% else %}.{% endif %} + {% endfor -%} + {% endif -%} + {% if v['default'] -%} + (default \C{@{ v['default'] }@}) + {% endif -%} + {% if v['version_added'] is defined -%} + (\I{* version @{ v['version_added'] }@}) + {% endif -%} + {% endfor -%} + {% endfor -%} +\end{xlist} +{% endif -%} + +{% if notes is defined -%} + {# -------- notes ----------#} + {% for note in notes -%} + \I{@{ note | jpfunc }@} + {% endfor %} +{% endif -%} + +{#------------------------------------------- + +{% if examples is defined -%} + {% for e in examples -%} + \begin{extymeta} +@{ e['code'] }@ + \end{extymeta} + {% endfor -%} +{% endif -%} +----------------------------------- #} + +{% if extradata is defined -%} +%--- BEGIN-EXTRADATA +\begin{extymeta} +@{ extradata -}@ +\end{extymeta} +%----- END-EXTRADATA +{% endif -%} diff --git a/hacking/templates/man.j2 b/hacking/templates/man.j2 new file mode 100644 index 00000000000..2f0ec848232 --- /dev/null +++ b/hacking/templates/man.j2 @@ -0,0 +1,64 @@ +.TH ANSIBLE.@{ module | upper }@ 5 "@{ now_date }@" "@{ ansible_version }@" "ANSIBLE MODULES" +." generated from @{ filename }@ +.SH NAME +@{ module }@ \- @{ short_description }@ +." ------ DESCRIPTION +.SH DESCRIPTION +{% for desc in description -%} +.PP +@{ desc | jpfunc }@ +{% endfor %} +." ------ OPTIONS +." +." +{% if options is defined -%} +.SH OPTIONS +{% for o in options -%}{% for opt, v in o.iteritems() -%} +.IP @{opt}@ +{% for desc in v.description -%} +@{ desc | jpfunc }@ +{% endfor -%} +{% if v['required'] %}(required){% endif -%} +{% if v['choices'] -%} +." .SS Choices +.IR Choices : +{% for choice in v['choices'] -%}@{ choice }@{% if not loop.last %},{% else %}.{% endif -%} +{% endfor -%} +{% endif %} +{#---------------------------------------------- #} +{% if v['version_added'] is defined -%} +(Added in Ansible version @{ v['version_added'] }@.) +{% endif -%} + {% endfor -%} + {% endfor -%} +{% endif %} +." +." +." ------ NOTES +{% if notes is defined -%} +.SH NOTES +{% for note in notes -%} +.PP +@{ note | jpfunc }@ +{% endfor -%} +{% endif -%} +." +." +." ------ EXAMPLES +{% if examples is defined -%} +.SH EXAMPLES +{% for e in examples -%} +.PP +.nf +@{ e['code'] }@ +.fi +{% endfor -%} +{% endif -%} +." ------- AUTHOR +{% if author is defined -%} +.SH AUTHOR +@{ author }@ +{% endif -%} +.SH SEE ALSO +.IR ansible (1), +.I http://ansible.github.com/modules.html#@{docuri}@ diff --git a/hacking/templates/rst.j2 b/hacking/templates/rst.j2 new file mode 100644 index 00000000000..def117d219d --- /dev/null +++ b/hacking/templates/rst.j2 @@ -0,0 +1,33 @@ +.. _@{ module }@: + +@{ module }@ +```````` + +{# ------------------------------------------ + # + # Please note: this looks like a core dump + # but it isn't one. + # + --------------------------------------------#} + +{% if version_added is defined -%} +.. versionadded:: @{ version_added }@ +{% endif %} + +{% for desc in description -%} +@{ desc | jpfunc }@ +{% endfor %} + +{% if options is defined -%} +@{ xline(10, "=") }@ @{xline(10)}@ @{xline(10)}@ @{xline(60)}@ +@{ "parameter" | fmt('%-10s')}@ @{ "required" | fmt('%-10s')}@ @{ "default" | fmt('%-10s')}@ @{ "comments" | fmt('%-60s')}@ +@{ xline(10, "=") }@ @{xline(10)}@ @{xline(10)}@ @{xline(60)}@ {% for o in options -%} + {% for opt, v in o.iteritems() %} +{% if v['required'] %}{% set required = 'yes' %}{% else %}{% set required = ' ' %}{% endif -%} +@{opt |fmt("%-10s") }@ @{ required|fmt('%-10s') }@ @{ v['default']|fmt('%-10s') }@ {% for desc in v.description -%}@{ desc | jpfunc }@{% endfor -%} + {% endfor -%} + {% endfor %} +@{ xline(10, "=") }@ @{xline(10)}@ @{xline(10)}@ @{xline(60)}@ +{% endif %} + + FIXME: examples!