~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Alexander Belchenko
  • Date: 2010-06-17 08:53:15 UTC
  • mfrom: (5300 +trunk)
  • mto: (5303.2.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5305.
  • Revision ID: bialix@ukr.net-20100617085315-hr8186zck57zn35s
merge bzr.dev; fix NEWS

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