/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

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

  • Committer: Robert Collins
  • Date: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

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