make_man.py 10.3 KB
Newer Older
1 2
#!/usr/bin/env python
# -*- coding: utf-8 -*-
3

4
# Copyright(C) 2010-2018 Laurent Bachelier
5
#
6
# This file is part of weboob.
7
#
8
# weboob is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Lesser General Public License as published by
10 11 12 13
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU Lesser General Public License for more details.
17
#
18
# You should have received a copy of the GNU Lesser General Public License
19
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
20

21
from __future__ import absolute_import, print_function
Romain Bignon's avatar
Romain Bignon committed
22 23 24 25 26 27 28 29 30

import imp
import inspect
import optparse
import os
import re
import sys
import tempfile
import time
31
from datetime import datetime
32
from textwrap import dedent
Romain Bignon's avatar
Romain Bignon committed
33

34
from weboob.tools.application.base import Application
35 36

BASE_PATH = os.path.join(os.path.dirname(__file__), os.pardir)
37
DEST_DIR = 'man'
38
COMP_PATH = 'tools/weboob_bash_completion'
39

40

41
class ManpageHelpFormatter(optparse.HelpFormatter):
42
    def __init__(self,
43 44 45 46 47
                 app,
                 indent_increment=0,
                 max_help_position=0,
                 width=80,
                 short_first=1):
48
        optparse.HelpFormatter.__init__(self, indent_increment, max_help_position, width, short_first)
49
        self.app = app
50 51 52 53

    def format_heading(self, heading):
        return ".SH %s\n" % heading.upper()

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
    def format_usage(self, usage):
        txt = ''
        for line in usage.split('\n'):
            line = line.lstrip().split(' ', 1)
            if len(txt) > 0:
                txt += '.br\n'
            txt += '.B %s\n' % line[0]

            arg_re = re.compile(r'([\[\s])([\w_]+)')
            args = re.sub(arg_re, r"\1\\fI\2\\fR", line[1])
            txt += args
            txt += '\n'
        return '.SH SYNOPSIS\n%s' % txt

    def format_description(self, description):
69 70 71
        desc = u'.SH DESCRIPTION\n.LP\n\n%s\n' % description
        if hasattr(self.app, 'CAPS'):
            self.app.weboob.modules_loader.load_all()
72
            caps = self.app.CAPS if isinstance(self.app.CAPS, tuple) else (self.app.CAPS,)
73
            modules = []
74
            for name, module in self.app.weboob.modules_loader.loaded.items():
75
                if module.has_caps(*caps):
76 77
                    modules.append(u'* %s (%s)' % (name, module.description))
            if len(modules) > 0:
78
                desc += u'\n.SS Supported websites:\n'
79
                desc += u'\n.br\n'.join(sorted(modules))
80
        return desc
81 82 83

    def format_commands(self, commands):
        s = u''
84
        for section, cmds in commands.items():
85 86
            if len(cmds) == 0:
                continue
87
            s += '.SH %s COMMANDS\n' % section.upper()
Romain Bignon's avatar
Romain Bignon committed
88
            for cmd in sorted(cmds):
89 90 91 92 93 94 95 96 97 98 99 100 101
                s += '.TP\n'
                h = cmd.split('\n')
                if ' ' in h[0]:
                    cmdname, args = h[0].split(' ', 1)
                    arg_re = re.compile(r'([A-Z_]+)')
                    args = re.sub(arg_re, r"\\fI\1\\fR", args)

                    s += '\\fB%s\\fR %s' % (cmdname, args)
                else:
                    s += '\\fB%s\\fR' % h[0]
                s += '%s\n' % '\n.br\n'.join(h[1:])
        return s

102 103 104
    def format_option_strings(self, option):
        opts = optparse.HelpFormatter.format_option_strings(self, option).split(", ")

105
        return ".TP\n" + ", ".join("\\fB%s\\fR" % opt for opt in opts)
106 107 108 109 110


def main():
    scripts_path = os.path.join(BASE_PATH, "scripts")
    files = os.listdir(scripts_path)
111
    completions = dict()
112 113 114 115 116 117 118 119 120

    # Create a fake "scripts" modules to import the scripts into
    sys.modules["scripts"] = imp.new_module("scripts")

    for fname in files:
        fpath = os.path.join(scripts_path, fname)
        if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
            with open(fpath) as f:
                # Python will likely want create a compiled file, we provide a place
121
                tmpdir = os.path.join(tempfile.gettempdir(), "weboob", "make_man")
122 123 124 125 126 127 128
                if not os.path.isdir(tmpdir):
                    os.makedirs(tmpdir)
                tmpfile = os.path.join(tmpdir, fname)

                desc = ("", "U", imp.PY_SOURCE)
                try:
                    script = imp.load_module("scripts.%s" % fname, f, tmpfile, desc)
129
                except ImportError as e:
130 131
                    print("Unable to load the %s script (%s)"
                          % (fname, e), file=sys.stderr)
132
                else:
133
                    print("Loaded %s" % fname)
134
                    # Find the applications we can handle
135
                    for klass in script.__dict__.values():
136
                        if inspect.isclass(klass) and issubclass(klass, Application) and klass.VERSION:
137
                            completions[fname] = analyze_application(klass, fname)
138 139
                finally:
                    # Cleanup compiled files if needed
140 141
                    if (os.path.isfile(tmpfile + "c")):
                        os.unlink(tmpfile + "c")
142
    write_completions(completions)
143

144 145 146 147

def format_title(title):
    return re.sub(r'^(.+):$', r'.SH \1\n.TP', title.group().upper())

148

149 150 151 152 153
# XXX useful because the PyQt QApplication destructor crashes sometimes. By
# keeping every applications until program end, it prevents to stop before
# every manpages have been generated. If it crashes at exit, it's not a
# really a problem.
applications = []
154 155


156 157
def analyze_application(app, script_name):
    application = app()
158
    applications.append(application)
159

160 161
    formatter = ManpageHelpFormatter(application)

162
    # patch the application
163 164
    application._parser.prog = "%s" % script_name
    application._parser.formatter = formatter
165 166 167 168 169
    helptext = application._parser.format_help(formatter)

    cmd_re = re.compile(r'^.+ Commands:$', re.MULTILINE)
    helptext = re.sub(cmd_re, format_title, helptext)
    helptext = helptext.replace("-", r"\-")
170
    coding = r'.\" -*- coding: utf-8 -*-'
171
    comment = r'.\" This file was generated automatically by tools/make_man.sh.'
172 173
    header = '.TH %s 1 "%s" "%s %s"' % (script_name.upper(), time.strftime("%d %B %Y"),
                                        script_name, app.VERSION.replace('.', '\\&.'))
174
    name = ".SH NAME\n%s \- %s" % (script_name, application.SHORT_DESCRIPTION)
175
    condition = """.SH CONDITION
176
The \-c and \-\-condition is a flexible way to filter and get only interesting results. It supports conditions on numerical values, dates, durations and strings. Dates are given in YYYY\-MM\-DD or YYYY\-MM\-DD HH:MM format. Durations look like XhYmZs where X, Y and Z are integers. Any of them may be omitted. For instance, YmZs, XhZs or Ym are accepted.
177 178 179
The syntax of one expression is "\\fBfield operator value\\fR". The field to test is always the left member of the expression.
.LP
The field is a member of the objects returned by the command. For example, a bank account has "balance", "coming" or "label" fields.
180
.SS The following operators are supported:
181 182
.TP
=
183
Test if object.field is equal to the value.
184 185
.TP
!=
186
Test if object.field is not equal to the value.
187 188
.TP
>
189
Test if object.field is greater than the value. If object.field is date, return true if value is before that object.field.
190 191
.TP
<
192
Test if object.field is less than the value. If object.field is date, return true if value is after that object.field.
193 194
.TP
|
195 196
This operator is available only for string fields. It works like the Unix standard \\fBgrep\\fR command, and returns True if the pattern specified in the value is in object.field.
.SS Expression combination
197 198 199 200
.LP
You can make a expression combinations with the keywords \\fB" AND "\\fR, \\fB" OR "\\fR an \\fB" LIMIT "\\fR.
.LP
The \\fBLIMIT\\fR keyword can be used to limit the number of items upon which running the expression. \\fBLIMIT\\fR can only be placed at the end of the expression followed by the number of elements you want.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
.SS Examples:
.nf
.B boobank ls \-\-condition 'label=Livret A'
.fi
Display only the "Livret A" account.
.PP
.nf
.B boobank ls \-\-condition 'balance>10000'
.fi
Display accounts with a lot of money.
.PP
.nf
.B boobank history account@backend \-\-condition 'label|rewe'
.fi
Get transactions containing "rewe".
.PP
.nf
218
.B boobank history account@backend \-\-condition 'date>2013\-12\-01 AND date<2013\-12\-09'
219 220
.fi
Get transactions betweens the 2th December and 8th December 2013.
221 222 223 224 225
.PP
.nf
.B boobank history account@backend \-\-condition 'date>2013\-12\-01  LIMIT 10'
.fi
Get transactions after the 2th December in the last 10 transactions
226
"""
227 228 229
    footer = """.SH COPYRIGHT
%s
.LP
230
For full copyright information see the COPYING file in the weboob package.
231 232 233
.LP
.RE
.SH FILES
234
 "~/.config/weboob/backends" """ % application.COPYRIGHT.replace('YEAR', '%d' % datetime.today().year)
235
    if len(app.CONFIG) > 0:
236
        footer += '\n\n "~/.config/weboob/%s"' % app.APPNAME
237

238
    # Skip internal applications.
Romain Bignon's avatar
Romain Bignon committed
239
    footer += "\n\n.SH SEE ALSO\nHome page: http://weboob.org/applications/%s" % application.APPNAME
240

241
    mantext = u"%s\n%s\n%s\n%s\n%s\n%s\n%s" % (coding, comment, header, name, helptext, condition, footer)
242
    with open(os.path.join(BASE_PATH, DEST_DIR, "%s.1" % script_name), 'w+') as manfile:
243
        for line in mantext.split('\n'):
244
            manfile.write('%s\n' % line.lstrip().encode('utf-8'))
245
    print("wrote %s/%s.1" % (DEST_DIR, script_name))
246

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
    return application._shell_completion_items()


def write_completions(completions):
    compscript = dedent('''
    # Weboob completion for Bash (automatically generated by tools/make_man.sh)
    #
    # vim: filetype=sh expandtab softtabstop=4 shiftwidth=4
    #
    # This file is part of weboob.
    #
    # This script can be distributed under the same license as the
    # weboob or bash packages.
    ''')
    for name, items in completions.items():
        compscript += dedent('''
        _weboob_{1}()
        {{
            local cur args

            COMPREPLY=()
            cur=${{COMP_WORDS[COMP_CWORD]}}
            args="{2}"

            COMPREPLY=( $(compgen -o default -W "${{args}}" -- "$cur" ) )
        }}
        complete -F _weboob_{1} {0}
        ''').format(name, name.replace('-', '_'), ' '.join(items))
    with open(os.path.join(BASE_PATH, COMP_PATH), 'w') as f:
        f.write(compscript)


279 280
if __name__ == '__main__':
    sys.exit(main())