/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: Jelmer Vernooij
  • Date: 2018-07-08 14:45:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7036.
  • Revision ID: jelmer@jelmer.uk-20180708144527-codhlvdcdg9y0nji
Fix a bunch of merge tests.

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