~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Parth Malwankar
  • Date: 2010-05-19 02:58:05 UTC
  • mfrom: (5240 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5241.
  • Revision ID: parth.malwankar@gmail.com-20100519025805-3gy1anwga6497y4s
merged in changes from trunk

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