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)
292
class DataCollector(object):
294
def __init__(self, no_plugins=False, selected_plugins=None):
295
self.data = CompletionData()
296
self.user_aliases = {}
298
self.selected_plugins = set()
299
elif selected_plugins is None:
300
self.selected_plugins = None
302
self.selected_plugins = {x.replace('-', '_')
303
for x in selected_plugins}
306
self.global_options()
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)
317
self.data.global_options.add(short)
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)
327
for name in sorted(commands.all_command_names()):
330
def command(self, name):
331
cmd = commands.get_cmd_object(name)
332
cmd_data = CommandData(name)
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):
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)
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]))
358
for optname, opt in sorted(opts.items()):
359
cmd_data.options.extend(self.option(opt))
361
if 'help' == name or 'help' in cmd.aliases:
362
cmd_data.fixed_words = ('($cmds %s)' %
363
" ".join(sorted(help_topics.topic_registry.keys())))
367
def option(self, opt):
369
parser = option.get_optparser({opt.name: opt})
370
parser = self.wrap_parser(optswitches, parser)
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)
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())
385
def wrap_container(self, optswitches, parser):
386
def tweaked_add_option(*opts, **attrs):
388
optswitches[name] = OptionData(name)
389
parser.add_option = tweaked_add_option
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)
401
def bash_completion_function(out, function_name="_brz", function_only=False,
403
no_plugins=False, selected_plugins=None):
404
dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
406
cg = BashCodeGen(data, function_name=function_name, debug=debug)
414
class cmd_bash_completion(commands.Command):
415
__doc__ = """Generate a shell function for bash command line completion.
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).
421
Commonly used like this:
422
eval "`brz bash-completion`"
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)"),
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']
444
bash_completion_function(sys.stdout, **kwargs)
447
if __name__ == '__main__':
452
def plugin_callback(option, opt, value, parser):
453
values = parser.values.selected_plugins
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()
475
parser.error("script does not take positional arguments")
477
for name, value in opts.__dict__.items():
478
if value is not None:
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)