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
397
def tweaked_add_option_group(*opts, **attrs):
398
return self.wrap_container(optswitches,
399
orig_add_option_group(*opts, **attrs))
400
parser.add_option_group = tweaked_add_option_group
401
return self.wrap_container(optswitches, parser)
404
def bash_completion_function(out, function_name="_brz", function_only=False,
406
no_plugins=False, selected_plugins=None):
407
dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
409
cg = BashCodeGen(data, function_name=function_name, debug=debug)
417
class cmd_bash_completion(commands.Command):
418
__doc__ = """Generate a shell function for bash command line completion.
420
This command generates a shell function which can be used by bash to
421
automatically complete the currently typed command when the user presses
422
the completion key (usually tab).
424
Commonly used like this:
425
eval "`brz bash-completion`"
429
option.Option("function-name", short_name="f", type=text_type, argname="name",
430
help="Name of the generated function (default: _brz)"),
431
option.Option("function-only", short_name="o", type=None,
432
help="Generate only the shell function, don't enable it"),
433
option.Option("debug", type=None, hidden=True,
434
help="Enable shell code useful for debugging"),
435
option.ListOption("plugin", type=text_type, argname="name",
436
# param_name="selected_plugins", # doesn't work, bug #387117
437
help="Enable completions for the selected plugin"
438
+ " (default: all plugins)"),
441
def run(self, **kwargs):
442
if 'plugin' in kwargs:
443
# work around bug #387117 which prevents us from using param_name
444
if len(kwargs['plugin']) > 0:
445
kwargs['selected_plugins'] = kwargs['plugin']
447
bash_completion_function(sys.stdout, **kwargs)
450
if __name__ == '__main__':
455
def plugin_callback(option, opt, value, parser):
456
values = parser.values.selected_plugins
462
parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
463
parser.add_option("--function-name", "-f", metavar="NAME",
464
help="Name of the generated function (default: _brz)")
465
parser.add_option("--function-only", "-o", action="store_true",
466
help="Generate only the shell function, don't enable it")
467
parser.add_option("--debug", action="store_true",
468
help=optparse.SUPPRESS_HELP)
469
parser.add_option("--no-plugins", action="store_true",
470
help="Don't load any brz plugins")
471
parser.add_option("--plugin", metavar="NAME", type="string",
472
dest="selected_plugins", default=[],
473
action="callback", callback=plugin_callback,
474
help="Enable completions for the selected plugin"
475
+ " (default: all plugins)")
476
(opts, args) = parser.parse_args()
478
parser.error("script does not take positional arguments")
480
for name, value in opts.__dict__.items():
481
if value is not None:
484
locale.setlocale(locale.LC_ALL, '')
485
if not kwargs.get('no_plugins', False):
486
plugin.load_plugins()
487
commands.install_bzr_command_hooks()
488
bash_completion_function(sys.stdout, **kwargs)