~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: John Arbash Meinel
  • Date: 2013-05-19 14:29:37 UTC
  • mfrom: (6437.63.9 2.5)
  • mto: (6437.63.10 2.5)
  • mto: This revision was merged to the branch mainline in revision 6575.
  • Revision ID: john@arbash-meinel.com-20130519142937-21ykz2n2y2f22za9
Merge in the actual 2.5 branch. It seems I failed before

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