/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: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

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 __future__ import absolute_import
 
20
 
 
21
from ... import (
 
22
    cmdline,
 
23
    commands,
 
24
    config,
 
25
    help_topics,
 
26
    option,
 
27
    plugin,
 
28
)
 
29
import breezy
 
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='_brz', 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 Breezy brz 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 brz %(brz_version)s
 
56
 
 
57
shopt -s progcomp
 
58
%(function)s
 
59
complete -F %(function_name)s -o default brz
 
60
""" % {
 
61
            "function_name": self.function_name,
 
62
            "function": self.function(),
 
63
            "brz_version": self.brz_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=( $(brz tags 2>/dev/null | sed 's/  *[^ ]*$//; s/ /\\\\\\\\ /g;') )
 
129
                ;;
 
130
        esac
 
131
        case $cur in
 
132
            [\\"\\']tag:*)
 
133
                fixedWords=( $(brz tags 2>/dev/null | sed 's/  *[^ ]*$//; s/^/tag:/') )
 
134
                ;;
 
135
            [\\"\\']*..tag:*)
 
136
                fixedWords=( $(brz 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 brz_version(self):
 
187
        brz_version = breezy.version_string
 
188
        if not self.data.plugins:
 
189
            brz_version += "."
 
190
        else:
 
191
            brz_version += " and the following plugins:"
 
192
            for name, plugin in sorted(self.data.plugins.items()):
 
193
                brz_version += "\n# %s" % plugin
 
194
        return brz_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 = breezy.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
    def __lt__(self, other):
 
289
        return 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])
 
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
 
 
395
        def tweaked_add_option_group(*opts, **attrs):
 
396
            return self.wrap_container(optswitches,
 
397
                                       orig_add_option_group(*opts, **attrs))
 
398
        parser.add_option_group = tweaked_add_option_group
 
399
        return self.wrap_container(optswitches, parser)
 
400
 
 
401
 
 
402
def bash_completion_function(out, function_name="_brz", function_only=False,
 
403
                             debug=False,
 
404
                             no_plugins=False, selected_plugins=None):
 
405
    dc = DataCollector(no_plugins=no_plugins,
 
406
                       selected_plugins=selected_plugins)
 
407
    data = dc.collect()
 
408
    cg = BashCodeGen(data, function_name=function_name, debug=debug)
 
409
    if function_only:
 
410
        res = cg.function()
 
411
    else:
 
412
        res = cg.script()
 
413
    out.write(res)
 
414
 
 
415
 
 
416
class cmd_bash_completion(commands.Command):
 
417
    __doc__ = """Generate a shell function for bash command line completion.
 
418
 
 
419
    This command generates a shell function which can be used by bash to
 
420
    automatically complete the currently typed command when the user presses
 
421
    the completion key (usually tab).
 
422
 
 
423
    Commonly used like this:
 
424
        eval "`brz bash-completion`"
 
425
    """
 
426
 
 
427
    takes_options = [
 
428
        option.Option("function-name", short_name="f", type=str, argname="name",
 
429
                      help="Name of the generated function (default: _brz)"),
 
430
        option.Option("function-only", short_name="o", type=None,
 
431
                      help="Generate only the shell function, don't enable it"),
 
432
        option.Option("debug", type=None, hidden=True,
 
433
                      help="Enable shell code useful for debugging"),
 
434
        option.ListOption("plugin", type=str, argname="name",
 
435
                          # param_name="selected_plugins", # doesn't work, bug #387117
 
436
                          help="Enable completions for the selected plugin"
 
437
                          + " (default: all plugins)"),
 
438
        ]
 
439
 
 
440
    def run(self, **kwargs):
 
441
        if 'plugin' in kwargs:
 
442
            # work around bug #387117 which prevents us from using param_name
 
443
            if len(kwargs['plugin']) > 0:
 
444
                kwargs['selected_plugins'] = kwargs['plugin']
 
445
            del kwargs['plugin']
 
446
        bash_completion_function(sys.stdout, **kwargs)
 
447
 
 
448
 
 
449
if __name__ == '__main__':
 
450
 
 
451
    import locale
 
452
    import optparse
 
453
 
 
454
    def plugin_callback(option, opt, value, parser):
 
455
        values = parser.values.selected_plugins
 
456
        if value == '-':
 
457
            del values[:]
 
458
        else:
 
459
            values.append(value)
 
460
 
 
461
    parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
 
462
    parser.add_option("--function-name", "-f", metavar="NAME",
 
463
                      help="Name of the generated function (default: _brz)")
 
464
    parser.add_option("--function-only", "-o", action="store_true",
 
465
                      help="Generate only the shell function, don't enable it")
 
466
    parser.add_option("--debug", action="store_true",
 
467
                      help=optparse.SUPPRESS_HELP)
 
468
    parser.add_option("--no-plugins", action="store_true",
 
469
                      help="Don't load any brz plugins")
 
470
    parser.add_option("--plugin", metavar="NAME", type="string",
 
471
                      dest="selected_plugins", default=[],
 
472
                      action="callback", callback=plugin_callback,
 
473
                      help="Enable completions for the selected plugin"
 
474
                      + " (default: all plugins)")
 
475
    (opts, args) = parser.parse_args()
 
476
    if args:
 
477
        parser.error("script does not take positional arguments")
 
478
    kwargs = dict()
 
479
    for name, value in opts.__dict__.items():
 
480
        if value is not None:
 
481
            kwargs[name] = value
 
482
 
 
483
    locale.setlocale(locale.LC_ALL, '')
 
484
    if not kwargs.get('no_plugins', False):
 
485
        plugin.load_plugins()
 
486
    commands.install_bzr_command_hooks()
 
487
    bash_completion_function(sys.stdout, **kwargs)