~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugins/bash_completion/bashcomp.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-19 10:58:39 UTC
  • mfrom: (6383 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6386.
  • Revision ID: jelmer@canonical.com-20111219105839-uji05ck4rkm1mj4j
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
# Copyright (C) 2009, 2010 Canonical Ltd
 
4
#
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from bzrlib import (
 
22
    cmdline,
 
23
    commands,
 
24
    config,
 
25
    help_topics,
 
26
    option,
 
27
    plugin,
 
28
)
 
29
import bzrlib
 
30
import re
 
31
 
 
32
 
 
33
class BashCodeGen(object):
 
34
    """Generate a bash script for given completion data."""
 
35
 
 
36
    def __init__(self, data, function_name='_bzr', debug=False):
 
37
        self.data = data
 
38
        self.function_name = function_name
 
39
        self.debug = debug
 
40
 
 
41
    def script(self):
 
42
        return ("""\
 
43
# Programmable completion for the Bazaar-NG bzr command under bash.
 
44
# Known to work with bash 2.05a as well as bash 4.1.2, and probably
 
45
# all versions in between as well.
 
46
 
 
47
# Based originally on the svn bash completition script.
 
48
# Customized by Sven Wilhelm/Icecrash.com
 
49
# Adjusted for automatic generation by Martin von Gagern
 
50
 
 
51
# Generated using the bash_completion plugin.
 
52
# See https://launchpad.net/bzr-bash-completion for details.
 
53
 
 
54
# Commands and options of bzr %(bzr_version)s
 
55
 
 
56
shopt -s progcomp
 
57
%(function)s
 
58
complete -F %(function_name)s -o default bzr
 
59
"""     % {
 
60
            "function_name": self.function_name,
 
61
            "function": self.function(),
 
62
            "bzr_version": self.bzr_version(),
 
63
        })
 
64
 
 
65
    def function(self):
 
66
        return ("""\
 
67
%(function_name)s ()
 
68
{
 
69
        local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts
 
70
        local curOpt optEnums
 
71
        local IFS=$' \\n'
 
72
 
 
73
        COMPREPLY=()
 
74
        cur=${COMP_WORDS[COMP_CWORD]}
 
75
 
 
76
        cmds='%(cmds)s'
 
77
        globalOpts=( %(global_options)s )
 
78
 
 
79
        # do ordinary expansion if we are anywhere after a -- argument
 
80
        for ((i = 1; i < COMP_CWORD; ++i)); do
 
81
                [[ ${COMP_WORDS[i]} == "--" ]] && return 0
 
82
        done
 
83
 
 
84
        # find the command; it's the first word not starting in -
 
85
        cmd=
 
86
        for ((cmdIdx = 1; cmdIdx < ${#COMP_WORDS[@]}; ++cmdIdx)); do
 
87
                if [[ ${COMP_WORDS[cmdIdx]} != -* ]]; then
 
88
                        cmd=${COMP_WORDS[cmdIdx]}
 
89
                        break
 
90
                fi
 
91
        done
 
92
 
 
93
        # complete command name if we are not already past the command
 
94
        if [[ $COMP_CWORD -le cmdIdx ]]; then
 
95
                COMPREPLY=( $( compgen -W "$cmds ${globalOpts[*]}" -- $cur ) )
 
96
                return 0
 
97
        fi
 
98
 
 
99
        # find the option for which we want to complete a value
 
100
        curOpt=
 
101
        if [[ $cur != -* ]] && [[ $COMP_CWORD -gt 1 ]]; then
 
102
                curOpt=${COMP_WORDS[COMP_CWORD - 1]}
 
103
                if [[ $curOpt == = ]]; then
 
104
                        curOpt=${COMP_WORDS[COMP_CWORD - 2]}
 
105
                elif [[ $cur == : ]]; then
 
106
                        cur=
 
107
                        curOpt="$curOpt:"
 
108
                elif [[ $curOpt == : ]]; then
 
109
                        curOpt=${COMP_WORDS[COMP_CWORD - 2]}:
 
110
                fi
 
111
        fi
 
112
%(debug)s
 
113
        cmdOpts=( )
 
114
        optEnums=( )
 
115
        fixedWords=( )
 
116
        case $cmd in
 
117
%(cases)s\
 
118
        *)
 
119
                cmdOpts=(--help -h)
 
120
                ;;
 
121
        esac
 
122
 
 
123
        IFS=$'\\n'
 
124
        if [[ ${#fixedWords[@]} -eq 0 ]] && [[ ${#optEnums[@]} -eq 0 ]] && [[ $cur != -* ]]; then
 
125
                case $curOpt in
 
126
                        tag:|*..tag:)
 
127
                                fixedWords=( $(bzr tags 2>/dev/null | sed 's/  *[^ ]*$//; s/ /\\\\\\\\ /g;') )
 
128
                                ;;
 
129
                esac
 
130
                case $cur in
 
131
                        [\\"\\']tag:*)
 
132
                                fixedWords=( $(bzr tags 2>/dev/null | sed 's/  *[^ ]*$//; s/^/tag:/') )
 
133
                                ;;
 
134
                        [\\"\\']*..tag:*)
 
135
                                fixedWords=( $(bzr tags 2>/dev/null | sed 's/  *[^ ]*$//') )
 
136
                                fixedWords=( $(for i in "${fixedWords[@]}"; do echo "${cur%%..tag:*}..tag:${i}"; done) )
 
137
                                ;;
 
138
                esac
 
139
        elif [[ $cur == = ]] && [[ ${#optEnums[@]} -gt 0 ]]; then
 
140
                # complete directly after "--option=", list all enum values
 
141
                COMPREPLY=( "${optEnums[@]}" )
 
142
                return 0
 
143
        else
 
144
                fixedWords=( "${cmdOpts[@]}"
 
145
                             "${globalOpts[@]}"
 
146
                             "${optEnums[@]}"
 
147
                             "${fixedWords[@]}" )
 
148
        fi
 
149
 
 
150
        if [[ ${#fixedWords[@]} -gt 0 ]]; then
 
151
                COMPREPLY=( $( compgen -W "${fixedWords[*]}" -- $cur ) )
 
152
        fi
 
153
 
 
154
        return 0
 
155
}
 
156
"""     % {
 
157
            "cmds": self.command_names(),
 
158
            "function_name": self.function_name,
 
159
            "cases": self.command_cases(),
 
160
            "global_options": self.global_options(),
 
161
            "debug": self.debug_output(),
 
162
        })
 
163
        # Help Emacs terminate strings: "
 
164
 
 
165
    def command_names(self):
 
166
        return " ".join(self.data.all_command_aliases())
 
167
 
 
168
    def debug_output(self):
 
169
        if not self.debug:
 
170
            return ''
 
171
        else:
 
172
            return (r"""
 
173
        # Debugging code enabled using the --debug command line switch.
 
174
        # Will dump some variables to the top portion of the terminal.
 
175
        echo -ne '\e[s\e[H'
 
176
        for (( i=0; i < ${#COMP_WORDS[@]}; ++i)); do
 
177
                echo "\$COMP_WORDS[$i]='${COMP_WORDS[i]}'"$'\e[K'
 
178
        done
 
179
        for i in COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY cur curOpt; do
 
180
                echo "\$${i}=\"${!i}\""$'\e[K'
 
181
        done
 
182
        echo -ne '---\e[K\e[u'
 
183
""")
 
184
 
 
185
    def bzr_version(self):
 
186
        bzr_version = bzrlib.version_string
 
187
        if not self.data.plugins:
 
188
            bzr_version += "."
 
189
        else:
 
190
            bzr_version += " and the following plugins:"
 
191
            for name, plugin in sorted(self.data.plugins.iteritems()):
 
192
                bzr_version += "\n# %s" % plugin
 
193
        return bzr_version
 
194
 
 
195
    def global_options(self):
 
196
        return " ".join(sorted(self.data.global_options))
 
197
 
 
198
    def command_cases(self):
 
199
        cases = ""
 
200
        for command in self.data.commands:
 
201
            cases += self.command_case(command)
 
202
        return cases
 
203
 
 
204
    def command_case(self, command):
 
205
        case = "\t%s)\n" % "|".join(command.aliases)
 
206
        if command.plugin:
 
207
            case += "\t\t# plugin \"%s\"\n" % command.plugin
 
208
        options = []
 
209
        enums = []
 
210
        for option in command.options:
 
211
            for message in option.error_messages:
 
212
                case += "\t\t# %s\n" % message
 
213
            if option.registry_keys:
 
214
                for key in option.registry_keys:
 
215
                    options.append("%s=%s" % (option, key))
 
216
                enums.append("%s) optEnums=( %s ) ;;" %
 
217
                             (option, ' '.join(option.registry_keys)))
 
218
            else:
 
219
                options.append(str(option))
 
220
        case += "\t\tcmdOpts=( %s )\n" % " ".join(options)
 
221
        if command.fixed_words:
 
222
            fixed_words = command.fixed_words
 
223
            if isinstance(fixed_words, list):
 
224
                fixed_words = "( %s )" + ' '.join(fixed_words)
 
225
            case += "\t\tfixedWords=%s\n" % fixed_words
 
226
        if enums:
 
227
            case += "\t\tcase $curOpt in\n\t\t\t"
 
228
            case += "\n\t\t\t".join(enums)
 
229
            case += "\n\t\tesac\n"
 
230
        case += "\t\t;;\n"
 
231
        return case
 
232
 
 
233
 
 
234
class CompletionData(object):
 
235
 
 
236
    def __init__(self):
 
237
        self.plugins = {}
 
238
        self.global_options = set()
 
239
        self.commands = []
 
240
 
 
241
    def all_command_aliases(self):
 
242
        for c in self.commands:
 
243
            for a in c.aliases:
 
244
                yield a
 
245
 
 
246
 
 
247
class CommandData(object):
 
248
 
 
249
    def __init__(self, name):
 
250
        self.name = name
 
251
        self.aliases = [name]
 
252
        self.plugin = None
 
253
        self.options = []
 
254
        self.fixed_words = None
 
255
 
 
256
 
 
257
class PluginData(object):
 
258
 
 
259
    def __init__(self, name, version=None):
 
260
        if version is None:
 
261
            try:
 
262
                version = bzrlib.plugin.plugins()[name].__version__
 
263
            except:
 
264
                version = 'unknown'
 
265
        self.name = name
 
266
        self.version = version
 
267
 
 
268
    def __str__(self):
 
269
        if self.version == 'unknown':
 
270
            return self.name
 
271
        return '%s %s' % (self.name, self.version)
 
272
 
 
273
 
 
274
class OptionData(object):
 
275
 
 
276
    def __init__(self, name):
 
277
        self.name = name
 
278
        self.registry_keys = None
 
279
        self.error_messages = []
 
280
 
 
281
    def __str__(self):
 
282
        return self.name
 
283
 
 
284
    def __cmp__(self, other):
 
285
        return cmp(self.name, other.name)
 
286
 
 
287
 
 
288
class DataCollector(object):
 
289
 
 
290
    def __init__(self, no_plugins=False, selected_plugins=None):
 
291
        self.data = CompletionData()
 
292
        self.user_aliases = {}
 
293
        if no_plugins:
 
294
            self.selected_plugins = set()
 
295
        elif selected_plugins is None:
 
296
            self.selected_plugins = None
 
297
        else:
 
298
            self.selected_plugins = set([x.replace('-', '_')
 
299
                                         for x in selected_plugins])
 
300
 
 
301
    def collect(self):
 
302
        self.global_options()
 
303
        self.aliases()
 
304
        self.commands()
 
305
        return self.data
 
306
 
 
307
    def global_options(self):
 
308
        re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
 
309
        help_text = help_topics.topic_registry.get_detail('global-options')
 
310
        for long, short in re_switch.findall(help_text):
 
311
            self.data.global_options.add(long)
 
312
            if short:
 
313
                self.data.global_options.add(short)
 
314
 
 
315
    def aliases(self):
 
316
        for alias, expansion in config.GlobalConfig().get_aliases().iteritems():
 
317
            for token in cmdline.split(expansion):
 
318
                if not token.startswith("-"):
 
319
                    self.user_aliases.setdefault(token, set()).add(alias)
 
320
                    break
 
321
 
 
322
    def commands(self):
 
323
        for name in sorted(commands.all_command_names()):
 
324
            self.command(name)
 
325
 
 
326
    def command(self, name):
 
327
        cmd = commands.get_cmd_object(name)
 
328
        cmd_data = CommandData(name)
 
329
 
 
330
        plugin_name = cmd.plugin_name()
 
331
        if plugin_name is not None:
 
332
            if (self.selected_plugins is not None and
 
333
                plugin not in self.selected_plugins):
 
334
                return None
 
335
            plugin_data = self.data.plugins.get(plugin_name)
 
336
            if plugin_data is None:
 
337
                plugin_data = PluginData(plugin_name)
 
338
                self.data.plugins[plugin_name] = plugin_data
 
339
            cmd_data.plugin = plugin_data
 
340
        self.data.commands.append(cmd_data)
 
341
 
 
342
        # Find all aliases to the command; both cmd-defined and user-defined.
 
343
        # We assume a user won't override one command with a different one,
 
344
        # but will choose completely new names or add options to existing
 
345
        # ones while maintaining the actual command name unchanged.
 
346
        cmd_data.aliases.extend(cmd.aliases)
 
347
        cmd_data.aliases.extend(sorted([useralias
 
348
            for cmdalias in cmd_data.aliases
 
349
            if cmdalias in self.user_aliases
 
350
            for useralias in self.user_aliases[cmdalias]
 
351
            if useralias not in cmd_data.aliases]))
 
352
 
 
353
        opts = cmd.options()
 
354
        for optname, opt in sorted(opts.iteritems()):
 
355
            cmd_data.options.extend(self.option(opt))
 
356
 
 
357
        if 'help' == name or 'help' in cmd.aliases:
 
358
            cmd_data.fixed_words = ('($cmds %s)' %
 
359
                " ".join(sorted(help_topics.topic_registry.keys())))
 
360
 
 
361
        return cmd_data
 
362
 
 
363
    def option(self, opt):
 
364
        optswitches = {}
 
365
        parser = option.get_optparser({opt.name: opt})
 
366
        parser = self.wrap_parser(optswitches, parser)
 
367
        optswitches.clear()
 
368
        opt.add_option(parser, opt.short_name())
 
369
        if isinstance(opt, option.RegistryOption) and opt.enum_switch:
 
370
            enum_switch = '--%s' % opt.name
 
371
            enum_data = optswitches.get(enum_switch)
 
372
            if enum_data:
 
373
                try:
 
374
                    enum_data.registry_keys = opt.registry.keys()
 
375
                except ImportError, e:
 
376
                    enum_data.error_messages.append(
 
377
                        "ERROR getting registry keys for '--%s': %s"
 
378
                        % (opt.name, str(e).split('\n')[0]))
 
379
        return sorted(optswitches.values())
 
380
 
 
381
    def wrap_container(self, optswitches, parser):
 
382
        def tweaked_add_option(*opts, **attrs):
 
383
            for name in opts:
 
384
                optswitches[name] = OptionData(name)
 
385
        parser.add_option = tweaked_add_option
 
386
        return parser
 
387
 
 
388
    def wrap_parser(self, optswitches, parser):
 
389
        orig_add_option_group = parser.add_option_group
 
390
        def tweaked_add_option_group(*opts, **attrs):
 
391
            return self.wrap_container(optswitches,
 
392
                orig_add_option_group(*opts, **attrs))
 
393
        parser.add_option_group = tweaked_add_option_group
 
394
        return self.wrap_container(optswitches, parser)
 
395
 
 
396
 
 
397
def bash_completion_function(out, function_name="_bzr", function_only=False,
 
398
                             debug=False,
 
399
                             no_plugins=False, selected_plugins=None):
 
400
    dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
 
401
    data = dc.collect()
 
402
    cg = BashCodeGen(data, function_name=function_name, debug=debug)
 
403
    if function_only:
 
404
        res = cg.function()
 
405
    else:
 
406
        res = cg.script()
 
407
    out.write(res)
 
408
 
 
409
 
 
410
class cmd_bash_completion(commands.Command):
 
411
    __doc__ = """Generate a shell function for bash command line completion.
 
412
 
 
413
    This command generates a shell function which can be used by bash to
 
414
    automatically complete the currently typed command when the user presses
 
415
    the completion key (usually tab).
 
416
    
 
417
    Commonly used like this:
 
418
        eval "`bzr bash-completion`"
 
419
    """
 
420
 
 
421
    takes_options = [
 
422
        option.Option("function-name", short_name="f", type=str, argname="name",
 
423
               help="Name of the generated function (default: _bzr)"),
 
424
        option.Option("function-only", short_name="o", type=None,
 
425
               help="Generate only the shell function, don't enable it"),
 
426
        option.Option("debug", type=None, hidden=True,
 
427
               help="Enable shell code useful for debugging"),
 
428
        option.ListOption("plugin", type=str, argname="name",
 
429
                # param_name="selected_plugins", # doesn't work, bug #387117
 
430
                help="Enable completions for the selected plugin"
 
431
                + " (default: all plugins)"),
 
432
        ]
 
433
 
 
434
    def run(self, **kwargs):
 
435
        import sys
 
436
        from bashcomp import bash_completion_function
 
437
        if 'plugin' in kwargs:
 
438
            # work around bug #387117 which prevents us from using param_name
 
439
            if len(kwargs['plugin']) > 0:
 
440
                kwargs['selected_plugins'] = kwargs['plugin']
 
441
            del kwargs['plugin']
 
442
        bash_completion_function(sys.stdout, **kwargs)
 
443
 
 
444
 
 
445
if __name__ == '__main__':
 
446
 
 
447
    import sys
 
448
    import locale
 
449
    import optparse
 
450
 
 
451
    def plugin_callback(option, opt, value, parser):
 
452
        values = parser.values.selected_plugins
 
453
        if value == '-':
 
454
            del values[:]
 
455
        else:
 
456
            values.append(value)
 
457
 
 
458
    parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
 
459
    parser.add_option("--function-name", "-f", metavar="NAME",
 
460
                      help="Name of the generated function (default: _bzr)")
 
461
    parser.add_option("--function-only", "-o", action="store_true",
 
462
                      help="Generate only the shell function, don't enable it")
 
463
    parser.add_option("--debug", action="store_true",
 
464
                      help=optparse.SUPPRESS_HELP)
 
465
    parser.add_option("--no-plugins", action="store_true",
 
466
                      help="Don't load any bzr plugins")
 
467
    parser.add_option("--plugin", metavar="NAME", type="string",
 
468
                      dest="selected_plugins", default=[],
 
469
                      action="callback", callback=plugin_callback,
 
470
                      help="Enable completions for the selected plugin"
 
471
                      + " (default: all plugins)")
 
472
    (opts, args) = parser.parse_args()
 
473
    if args:
 
474
        parser.error("script does not take positional arguments")
 
475
    kwargs = dict()
 
476
    for name, value in opts.__dict__.iteritems():
 
477
        if value is not None:
 
478
            kwargs[name] = value
 
479
 
 
480
    locale.setlocale(locale.LC_ALL, '')
 
481
    if not kwargs.get('no_plugins', False):
 
482
        plugin.load_plugins()
 
483
    commands.install_bzr_command_hooks()
 
484
    bash_completion_function(sys.stdout, **kwargs)