/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-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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
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
    def __lt__(self, other):
 
292
        return self.name < other.name
 
293
 
 
294
 
 
295
class DataCollector(object):
 
296
 
 
297
    def __init__(self, no_plugins=False, selected_plugins=None):
 
298
        self.data = CompletionData()
 
299
        self.user_aliases = {}
 
300
        if no_plugins:
 
301
            self.selected_plugins = set()
 
302
        elif selected_plugins is None:
 
303
            self.selected_plugins = None
 
304
        else:
 
305
            self.selected_plugins = {x.replace('-', '_')
 
306
                                     for x in selected_plugins}
 
307
 
 
308
    def collect(self):
 
309
        self.global_options()
 
310
        self.aliases()
 
311
        self.commands()
 
312
        return self.data
 
313
 
 
314
    def global_options(self):
 
315
        re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
 
316
        help_text = help_topics.topic_registry.get_detail('global-options')
 
317
        for long, short in re_switch.findall(help_text):
 
318
            self.data.global_options.add(long)
 
319
            if short:
 
320
                self.data.global_options.add(short)
 
321
 
 
322
    def aliases(self):
 
323
        for alias, expansion in config.GlobalConfig().get_aliases().items():
 
324
            for token in cmdline.split(expansion):
 
325
                if not token.startswith("-"):
 
326
                    self.user_aliases.setdefault(token, set()).add(alias)
 
327
                    break
 
328
 
 
329
    def commands(self):
 
330
        for name in sorted(commands.all_command_names()):
 
331
            self.command(name)
 
332
 
 
333
    def command(self, name):
 
334
        cmd = commands.get_cmd_object(name)
 
335
        cmd_data = CommandData(name)
 
336
 
 
337
        plugin_name = cmd.plugin_name()
 
338
        if plugin_name is not None:
 
339
            if (self.selected_plugins is not None and
 
340
                    plugin not in self.selected_plugins):
 
341
                return None
 
342
            plugin_data = self.data.plugins.get(plugin_name)
 
343
            if plugin_data is None:
 
344
                plugin_data = PluginData(plugin_name)
 
345
                self.data.plugins[plugin_name] = plugin_data
 
346
            cmd_data.plugin = plugin_data
 
347
        self.data.commands.append(cmd_data)
 
348
 
 
349
        # Find all aliases to the command; both cmd-defined and user-defined.
 
350
        # We assume a user won't override one command with a different one,
 
351
        # but will choose completely new names or add options to existing
 
352
        # ones while maintaining the actual command name unchanged.
 
353
        cmd_data.aliases.extend(cmd.aliases)
 
354
        cmd_data.aliases.extend(sorted([useralias
 
355
                                        for cmdalias in cmd_data.aliases
 
356
                                        if cmdalias in self.user_aliases
 
357
                                        for useralias in self.user_aliases[cmdalias]
 
358
                                        if useralias not in cmd_data.aliases]))
 
359
 
 
360
        opts = cmd.options()
 
361
        for optname, opt in sorted(opts.items()):
 
362
            cmd_data.options.extend(self.option(opt))
 
363
 
 
364
        if 'help' == name or 'help' in cmd.aliases:
 
365
            cmd_data.fixed_words = ('($cmds %s)' %
 
366
                                    " ".join(sorted(help_topics.topic_registry.keys())))
 
367
 
 
368
        return cmd_data
 
369
 
 
370
    def option(self, opt):
 
371
        optswitches = {}
 
372
        parser = option.get_optparser([opt])
 
373
        parser = self.wrap_parser(optswitches, parser)
 
374
        optswitches.clear()
 
375
        opt.add_option(parser, opt.short_name())
 
376
        if isinstance(opt, option.RegistryOption) and opt.enum_switch:
 
377
            enum_switch = '--%s' % opt.name
 
378
            enum_data = optswitches.get(enum_switch)
 
379
            if enum_data:
 
380
                try:
 
381
                    enum_data.registry_keys = opt.registry.keys()
 
382
                except ImportError as e:
 
383
                    enum_data.error_messages.append(
 
384
                        "ERROR getting registry keys for '--%s': %s"
 
385
                        % (opt.name, str(e).split('\n')[0]))
 
386
        return sorted(optswitches.values())
 
387
 
 
388
    def wrap_container(self, optswitches, parser):
 
389
        def tweaked_add_option(*opts, **attrs):
 
390
            for name in opts:
 
391
                optswitches[name] = OptionData(name)
 
392
        parser.add_option = tweaked_add_option
 
393
        return parser
 
394
 
 
395
    def wrap_parser(self, optswitches, parser):
 
396
        orig_add_option_group = parser.add_option_group
 
397
 
 
398
        def tweaked_add_option_group(*opts, **attrs):
 
399
            return self.wrap_container(optswitches,
 
400
                                       orig_add_option_group(*opts, **attrs))
 
401
        parser.add_option_group = tweaked_add_option_group
 
402
        return self.wrap_container(optswitches, parser)
 
403
 
 
404
 
 
405
def bash_completion_function(out, function_name="_brz", function_only=False,
 
406
                             debug=False,
 
407
                             no_plugins=False, selected_plugins=None):
 
408
    dc = DataCollector(no_plugins=no_plugins,
 
409
                       selected_plugins=selected_plugins)
 
410
    data = dc.collect()
 
411
    cg = BashCodeGen(data, function_name=function_name, debug=debug)
 
412
    if function_only:
 
413
        res = cg.function()
 
414
    else:
 
415
        res = cg.script()
 
416
    out.write(res)
 
417
 
 
418
 
 
419
class cmd_bash_completion(commands.Command):
 
420
    __doc__ = """Generate a shell function for bash command line completion.
 
421
 
 
422
    This command generates a shell function which can be used by bash to
 
423
    automatically complete the currently typed command when the user presses
 
424
    the completion key (usually tab).
 
425
 
 
426
    Commonly used like this:
 
427
        eval "`brz bash-completion`"
 
428
    """
 
429
 
 
430
    takes_options = [
 
431
        option.Option("function-name", short_name="f", type=text_type, argname="name",
 
432
                      help="Name of the generated function (default: _brz)"),
 
433
        option.Option("function-only", short_name="o", type=None,
 
434
                      help="Generate only the shell function, don't enable it"),
 
435
        option.Option("debug", type=None, hidden=True,
 
436
                      help="Enable shell code useful for debugging"),
 
437
        option.ListOption("plugin", type=text_type, argname="name",
 
438
                          # param_name="selected_plugins", # doesn't work, bug #387117
 
439
                          help="Enable completions for the selected plugin"
 
440
                          + " (default: all plugins)"),
 
441
        ]
 
442
 
 
443
    def run(self, **kwargs):
 
444
        if 'plugin' in kwargs:
 
445
            # work around bug #387117 which prevents us from using param_name
 
446
            if len(kwargs['plugin']) > 0:
 
447
                kwargs['selected_plugins'] = kwargs['plugin']
 
448
            del kwargs['plugin']
 
449
        bash_completion_function(sys.stdout, **kwargs)
 
450
 
 
451
 
 
452
if __name__ == '__main__':
 
453
 
 
454
    import locale
 
455
    import optparse
 
456
 
 
457
    def plugin_callback(option, opt, value, parser):
 
458
        values = parser.values.selected_plugins
 
459
        if value == '-':
 
460
            del values[:]
 
461
        else:
 
462
            values.append(value)
 
463
 
 
464
    parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
 
465
    parser.add_option("--function-name", "-f", metavar="NAME",
 
466
                      help="Name of the generated function (default: _brz)")
 
467
    parser.add_option("--function-only", "-o", action="store_true",
 
468
                      help="Generate only the shell function, don't enable it")
 
469
    parser.add_option("--debug", action="store_true",
 
470
                      help=optparse.SUPPRESS_HELP)
 
471
    parser.add_option("--no-plugins", action="store_true",
 
472
                      help="Don't load any brz plugins")
 
473
    parser.add_option("--plugin", metavar="NAME", type="string",
 
474
                      dest="selected_plugins", default=[],
 
475
                      action="callback", callback=plugin_callback,
 
476
                      help="Enable completions for the selected plugin"
 
477
                      + " (default: all plugins)")
 
478
    (opts, args) = parser.parse_args()
 
479
    if args:
 
480
        parser.error("script does not take positional arguments")
 
481
    kwargs = dict()
 
482
    for name, value in opts.__dict__.items():
 
483
        if value is not None:
 
484
            kwargs[name] = value
 
485
 
 
486
    locale.setlocale(locale.LC_ALL, '')
 
487
    if not kwargs.get('no_plugins', False):
 
488
        plugin.load_plugins()
 
489
    commands.install_bzr_command_hooks()
 
490
    bash_completion_function(sys.stdout, **kwargs)