/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-08-23 01:15:41 UTC
  • mfrom: (7520.1.4 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200823011541-nv0oh7nzaganx2qy
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/389690

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)