3
# Copyright (C) 2009, 2010 Canonical Ltd
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.
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.
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
19
from __future__ import absolute_import
29
from ...sixish import (
37
class BashCodeGen(object):
38
"""Generate a bash script for given completion data."""
40
def __init__(self, data, function_name='_brz', debug=False):
42
self.function_name = function_name
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.
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
55
# Generated using the bash_completion plugin.
56
# See https://launchpad.net/bzr-bash-completion for details.
58
# Commands and options of brz %(brz_version)s
62
complete -F %(function_name)s -o default brz
64
"function_name": self.function_name,
65
"function": self.function(),
66
"brz_version": self.brz_version(),
73
local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts
78
cur=${COMP_WORDS[COMP_CWORD]}
81
globalOpts=( %(global_options)s )
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
88
# find the command; it's the first word not starting in -
90
for ((cmdIdx = 1; cmdIdx < ${#COMP_WORDS[@]}; ++cmdIdx)); do
91
if [[ ${COMP_WORDS[cmdIdx]} != -* ]]; then
92
cmd=${COMP_WORDS[cmdIdx]}
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 ) )
103
# find the option for which we want to complete a value
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
112
elif [[ $curOpt == : ]]; then
113
curOpt=${COMP_WORDS[COMP_CWORD - 2]}:
128
if [[ ${#fixedWords[@]} -eq 0 ]] && [[ ${#optEnums[@]} -eq 0 ]] && [[ $cur != -* ]]; then
131
fixedWords=( $(brz tags 2>/dev/null | sed 's/ *[^ ]*$//; s/ /\\\\\\\\ /g;') )
136
fixedWords=( $(brz tags 2>/dev/null | sed 's/ *[^ ]*$//; s/^/tag:/') )
139
fixedWords=( $(brz tags 2>/dev/null | sed 's/ *[^ ]*$//') )
140
fixedWords=( $(for i in "${fixedWords[@]}"; do echo "${cur%%..tag:*}..tag:${i}"; done) )
143
elif [[ $cur == = ]] && [[ ${#optEnums[@]} -gt 0 ]]; then
144
# complete directly after "--option=", list all enum values
145
COMPREPLY=( "${optEnums[@]}" )
148
fixedWords=( "${cmdOpts[@]}"
154
if [[ ${#fixedWords[@]} -gt 0 ]]; then
155
COMPREPLY=( $( compgen -W "${fixedWords[*]}" -- $cur ) )
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(),
167
# Help Emacs terminate strings: "
169
def command_names(self):
170
return " ".join(self.data.all_command_aliases())
172
def debug_output(self):
177
# Debugging code enabled using the --debug command line switch.
178
# Will dump some variables to the top portion of the terminal.
180
for (( i=0; i < ${#COMP_WORDS[@]}; ++i)); do
181
echo "\$COMP_WORDS[$i]='${COMP_WORDS[i]}'"$'\e[K'
183
for i in COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY cur curOpt; do
184
echo "\$${i}=\"${!i}\""$'\e[K'
186
echo -ne '---\e[K\e[u'
189
def brz_version(self):
190
brz_version = breezy.version_string
191
if not self.data.plugins:
194
brz_version += " and the following plugins:"
195
for name, plugin in sorted(self.data.plugins.items()):
196
brz_version += "\n# %s" % plugin
199
def global_options(self):
200
return " ".join(sorted(self.data.global_options))
202
def command_cases(self):
204
for command in self.data.commands:
205
cases += self.command_case(command)
208
def command_case(self, command):
209
case = "\t%s)\n" % "|".join(command.aliases)
211
case += "\t\t# plugin \"%s\"\n" % command.plugin
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)))
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
231
case += "\t\tcase $curOpt in\n\t\t\t"
232
case += "\n\t\t\t".join(enums)
233
case += "\n\t\tesac\n"
238
class CompletionData(object):
242
self.global_options = set()
245
def all_command_aliases(self):
246
for c in self.commands:
251
class CommandData(object):
253
def __init__(self, name):
255
self.aliases = [name]
258
self.fixed_words = None
261
class PluginData(object):
263
def __init__(self, name, version=None):
266
version = breezy.plugin.plugins()[name].__version__
270
self.version = version
273
if self.version == 'unknown':
275
return '%s %s' % (self.name, self.version)
278
class OptionData(object):
280
def __init__(self, name):
282
self.registry_keys = None
283
self.error_messages = []
288
def __cmp__(self, other):
289
return cmp(self.name, other.name)
291
def __lt__(self, other):
292
return self.name < other.name
295
class DataCollector(object):
297
def __init__(self, no_plugins=False, selected_plugins=None):
298
self.data = CompletionData()
299
self.user_aliases = {}
301
self.selected_plugins = set()
302
elif selected_plugins is None:
303
self.selected_plugins = None
305
self.selected_plugins = {x.replace('-', '_')
306
for x in selected_plugins}
309
self.global_options()
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)
320
self.data.global_options.add(short)
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)
330
for name in sorted(commands.all_command_names()):
333
def command(self, name):
334
cmd = commands.get_cmd_object(name)
335
cmd_data = CommandData(name)
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):
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)
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]))
361
for optname, opt in sorted(opts.items()):
362
cmd_data.options.extend(self.option(opt))
364
if 'help' == name or 'help' in cmd.aliases:
365
cmd_data.fixed_words = ('($cmds %s)' %
366
" ".join(sorted(help_topics.topic_registry.keys())))
370
def option(self, opt):
372
parser = option.get_optparser([opt])
373
parser = self.wrap_parser(optswitches, parser)
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)
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())
388
def wrap_container(self, optswitches, parser):
389
def tweaked_add_option(*opts, **attrs):
391
optswitches[name] = OptionData(name)
392
parser.add_option = tweaked_add_option
395
def wrap_parser(self, optswitches, parser):
396
orig_add_option_group = parser.add_option_group
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)
405
def bash_completion_function(out, function_name="_brz", function_only=False,
407
no_plugins=False, selected_plugins=None):
408
dc = DataCollector(no_plugins=no_plugins,
409
selected_plugins=selected_plugins)
411
cg = BashCodeGen(data, function_name=function_name, debug=debug)
419
class cmd_bash_completion(commands.Command):
420
__doc__ = """Generate a shell function for bash command line completion.
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).
426
Commonly used like this:
427
eval "`brz bash-completion`"
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)"),
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']
449
bash_completion_function(sys.stdout, **kwargs)
452
if __name__ == '__main__':
457
def plugin_callback(option, opt, value, parser):
458
values = parser.values.selected_plugins
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()
480
parser.error("script does not take positional arguments")
482
for name, value in opts.__dict__.items():
483
if value is not None:
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)