make_man.py 10.3 KB
Newer Older
1
2
#!/usr/bin/env python
# -*- coding: utf-8 -*-
Laurent Bachelier's avatar
Laurent Bachelier committed
3

4
# Copyright(C) 2010-2018 Laurent Bachelier
5
#
Romain Bignon's avatar
Romain Bignon committed
6
# This file is part of weboob.
7
#
Romain Bignon's avatar
Romain Bignon committed
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
Romain Bignon's avatar
Romain Bignon committed
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
Romain Bignon's avatar
Romain Bignon committed
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
Romain Bignon's avatar
Romain Bignon committed
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:
Laurent Bachelier's avatar
Laurent Bachelier committed
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.'
Romain Bignon's avatar
Romain Bignon committed
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)
Florent's avatar
Florent committed
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.
Florent's avatar
Florent committed
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:
Florent's avatar
Florent committed
181
182
.TP
=
183
Test if object.field is equal to the value.
Florent's avatar
Florent committed
184
185
.TP
!=
186
Test if object.field is not equal to the value.
Florent's avatar
Florent committed
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.
Florent's avatar
Florent committed
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.
Florent's avatar
Florent committed
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.
Florent's avatar
Florent committed
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
Florent's avatar
Florent committed
218
.B boobank history account@backend \-\-condition 'date>2013\-12\-01 AND date<2013\-12\-09'
Florent's avatar
Florent committed
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
Florent's avatar
Florent committed
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())