From b762eed1715d30d5e1de1e49e2862f2c34129d2f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Sep 2016 00:16:56 -0400 Subject: [PATCH] Remove home-made bash completion infrastructure In favor of Click's built-in support. The script is now generated using `_BEET_COMPLETE=source beet` instead of `beet completion`. We should strongly consider restoring the `beet completion` command just for backwards compatibility. All of this needs to be heavily publicized in the changelog. --- MANIFEST.in | 3 - beets/ui/commands.py | 110 --------------------- beets/ui/completion_base.sh | 162 ------------------------------ docs/reference/cli.rst | 10 +- test/rsrc/test_completion.sh | 185 ----------------------------------- test/test_ui.py | 53 +--------- 6 files changed, 4 insertions(+), 519 deletions(-) delete mode 100644 beets/ui/completion_base.sh delete mode 100644 test/rsrc/test_completion.sh diff --git a/MANIFEST.in b/MANIFEST.in index f5459a443..122f1c2bc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -29,8 +29,5 @@ global-exclude .DS_Store # Include default config include beets/config_default.yaml -# Shell completion template -include beets/ui/completion_base.sh - # Include extra bits recursive-include extra * diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 2d0ef150c..19a537db2 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -20,7 +20,6 @@ interface. from __future__ import division, absolute_import, print_function import os -import re from platform import python_version from collections import namedtuple, Counter from itertools import chain @@ -39,7 +38,6 @@ from beets.util import syspath, normpath, ancestry, displayable_path from beets import library from beets import config from beets import logging -from beets.util.confit import _package_path import six VARIOUS_ARTISTS = u'Various Artists' @@ -1660,111 +1658,3 @@ config_cmd.parser.add_option( ) config_cmd.func = config_func default_commands.append(config_cmd) - - -# completion: print completion script - -def print_completion(*args): - for line in completion_script(default_commands + plugins.commands()): - print_(line, end=u'') - if not any(map(os.path.isfile, BASH_COMPLETION_PATHS)): - log.warning(u'Warning: Unable to find the bash-completion package. ' - u'Command line completion might not work.') - -BASH_COMPLETION_PATHS = map(syspath, [ - u'/etc/bash_completion', - u'/usr/share/bash-completion/bash_completion', - u'/usr/local/share/bash-completion/bash_completion', - # SmartOS - u'/opt/local/share/bash-completion/bash_completion', - # Homebrew (before bash-completion2) - u'/usr/local/etc/bash_completion', -]) - - -def completion_script(commands): - """Yield the full completion shell script as strings. - - ``commands`` is alist of ``ui.Subcommand`` instances to generate - completion data for. - """ - base_script = os.path.join(_package_path('beets.ui'), 'completion_base.sh') - with open(base_script, 'r') as base_script: - yield util.text_string(base_script.read()) - - options = {} - aliases = {} - command_names = [] - - # Collect subcommands - for cmd in commands: - name = cmd.name - command_names.append(name) - - for alias in cmd.aliases: - if re.match(r'^\w+$', alias): - aliases[alias] = name - - options[name] = {u'flags': [], u'opts': []} - for opts in cmd.parser._get_all_options()[1:]: - if opts.action in ('store_true', 'store_false'): - option_type = u'flags' - else: - option_type = u'opts' - - options[name][option_type].extend( - opts._short_opts + opts._long_opts - ) - - # Add global options - options['_global'] = { - u'flags': [u'-v', u'--verbose'], - u'opts': - u'-l --library -c --config -d --directory -h --help'.split(u' ') - } - - # Add flags common to all commands - options['_common'] = { - u'flags': [u'-h', u'--help'] - } - - # Start generating the script - yield u"_beet() {\n" - - # Command names - yield u" local commands='%s'\n" % ' '.join(command_names) - yield u"\n" - - # Command aliases - yield u" local aliases='%s'\n" % ' '.join(aliases.keys()) - for alias, cmd in aliases.items(): - yield u" local alias__%s=%s\n" % (alias, cmd) - yield u'\n' - - # Fields - yield u" fields='%s'\n" % ' '.join( - set( - list(library.Item._fields.keys()) + - list(library.Album._fields.keys()) - ) - ) - - # Command options - for cmd, opts in options.items(): - for option_type, option_list in opts.items(): - if option_list: - option_list = u' '.join(option_list) - yield u" local %s__%s='%s'\n" % ( - option_type, cmd, option_list) - - yield u' _beet_dispatch\n' - yield u'}\n' - - -completion_cmd = ui.Subcommand( - 'completion', - help=u'print shell script that provides command line completion' -) -completion_cmd.func = print_completion -completion_cmd.hide = True -default_commands.append(completion_cmd) diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh deleted file mode 100644 index ce3fb6e27..000000000 --- a/beets/ui/completion_base.sh +++ /dev/null @@ -1,162 +0,0 @@ -# This file is part of beets. -# Copyright (c) 2014, Thomas Scholtes. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - - - -# Completion for the `beet` command -# ================================= -# -# Load this script to complete beets subcommands, options, and -# queries. -# -# If a beets command is found on the command line it completes filenames and -# the subcommand's options. Otherwise it will complete global options and -# subcommands. If the previous option on the command line expects an argument, -# it also completes filenames or directories. Options are only -# completed if '-' has already been typed on the command line. -# -# Note that completion of plugin commands only works for those plugins -# that were enabled when running `beet completion`. It does not check -# plugins dynamically -# -# Currently, only Bash 3.2 and newer is supported and the -# `bash-completion` package is requied. -# -# TODO -# ---- -# -# * There are some issues with arguments that are quoted on the command line. -# -# * Complete arguments for the `--format` option by expanding field variables. -# -# beet ls -f "$tit[TAB] -# beet ls -f "$title -# -# * Support long options with `=`, e.g. `--config=file`. Debian's bash -# completion package can handle this. -# - - -# Determines the beets subcommand and dispatches the completion -# accordingly. -_beet_dispatch() { - local cur prev cmd= - - COMPREPLY=() - _get_comp_words_by_ref -n : cur prev - - # Look for the beets subcommand - local arg - for (( i=1; i < COMP_CWORD; i++ )); do - arg="${COMP_WORDS[i]}" - if _list_include_item "${opts___global}" $arg; then - ((i++)) - elif [[ "$arg" != -* ]]; then - cmd="$arg" - break - fi - done - - # Replace command shortcuts - if [[ -n $cmd ]] && _list_include_item "$aliases" "$cmd"; then - eval "cmd=\$alias__$cmd" - fi - - case $cmd in - help) - COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) - ;; - list|remove|move|update|write|stats) - _beet_complete_query - ;; - "") - _beet_complete_global - ;; - *) - _beet_complete - ;; - esac -} - - -# Adds option and file completion to COMPREPLY for the subcommand $cmd -_beet_complete() { - if [[ $cur == -* ]]; then - local opts flags completions - eval "opts=\$opts__$cmd" - eval "flags=\$flags__$cmd" - completions="${flags___common} ${opts} ${flags}" - COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) - else - _filedir - fi -} - - -# Add global options and subcommands to the completion -_beet_complete_global() { - case $prev in - -h|--help) - # Complete commands - COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) - return - ;; - -l|--library|-c|--config) - # Filename completion - _filedir - return - ;; - -d|--directory) - # Directory completion - _filedir -d - return - ;; - esac - - if [[ $cur == -* ]]; then - local completions="$opts___global $flags___global" - COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) - elif [[ -n $cur ]] && _list_include_item "$aliases" "$cur"; then - local cmd - eval "cmd=\$alias__$cur" - COMPREPLY+=( "$cmd" ) - else - COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) - fi -} - -_beet_complete_query() { - local opts - eval "opts=\$opts__$cmd" - - if [[ $cur == -* ]] || _list_include_item "$opts" "$prev"; then - _beet_complete - elif [[ $cur != \'* && $cur != \"* && - $cur != *:* ]]; then - # Do not complete quoted queries or those who already have a field - # set. - compopt -o nospace - COMPREPLY+=( $(compgen -S : -W "$fields" -- $cur) ) - return 0 - fi -} - -# Returns true if the space separated list $1 includes $2 -_list_include_item() { - [[ " $1 " == *[[:space:]]$2[[:space:]]* ]] -} - -# This is where beets dynamically adds the _beet function. This -# function sets the variables $flags, $opts, $commands, and $aliases. -complete -o filenames -F _beet beet diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 93b9b6253..e1581d7bf 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -404,10 +404,10 @@ Beets includes support for shell command completion. The command ``beet completion`` prints out a `bash`_ 3.2 script; to enable completion put a line like this into your ``.bashrc`` or similar file:: - eval "$(beet completion)" + eval "$(_BEET_COMPLETE=source beet)" -Or, to avoid slowing down your shell startup time, you can pipe the ``beet -completion`` output to a file and source that instead. +Or, to avoid slowing down your shell startup time, you can pipe the +``_BEET_COMPLETE=source beet`` output to a file and source that instead. You will also need to source the `bash-completion`_ script, which is probably available via your package manager. On OS X, you can install it via Homebrew @@ -429,10 +429,6 @@ accepts a query, the script will also complete field names. :: (Don't worry about the slash in front of the colon: this is a escape sequence for the shell and won't be seen by beets.) -Completion of plugin commands only works for those plugins -that were enabled when running ``beet completion``. If you add a plugin -later on you will want to re-generate the script. - zsh ``` diff --git a/test/rsrc/test_completion.sh b/test/rsrc/test_completion.sh deleted file mode 100644 index 88a0aca9b..000000000 --- a/test/rsrc/test_completion.sh +++ /dev/null @@ -1,185 +0,0 @@ -# Function stub -compopt() { return 0; } - -initcli() { - COMP_WORDS=( "beet" "$@" ) - let COMP_CWORD=${#COMP_WORDS[@]}-1 - COMP_LINE="${COMP_WORDS[@]}" - let COMP_POINT=${#COMP_LINE} - _beet -} - -completes() { - for word in "$@"; do - [[ " ${COMPREPLY[@]} " == *[[:space:]]$word[[:space:]]* ]] || return 1 - done -} - -COMMANDS='fields import list update remove - stats version modify move write - help' - -HELP_OPTS='-h --help' - - -test_commands() { - initcli '' && - completes $COMMANDS && - - initcli -v '' && - completes $COMMANDS && - - initcli -l help '' && - completes $COMMANDS && - - initcli -d list '' && - completes $COMMANDS && - - initcli -h '' && - completes $COMMANDS && - true -} - -test_command_aliases() { - initcli ls && - completes list && - - initcli l && - ! completes ls && - - initcli im && - completes import && - true -} - -test_global_opts() { - initcli - && - completes \ - -l --library \ - -d --directory \ - -h --help \ - -c --config \ - -v --verbose && - true -} - - -test_global_file_opts() { - # FIXME somehow file completion only works when the completion - # function is called by the shell completion utilities. So we can't - # test it here - initcli --library '' && - completes $(compgen -d) && - - initcli -l '' && - completes $(compgen -d) && - - initcli --config '' && - completes $(compgen -d) && - - initcli -c '' && - completes $(compgen -d) && - true -} - - -test_global_dir_opts() { - initcli --directory '' && - completes $(compgen -d) && - - initcli -d '' && - completes $(compgen -d) && - true -} - - -test_fields_command() { - initcli fields - && - completes -h --help && - - initcli fields '' && - completes $(compgen -d) && - true -} - - -test_import_files() { - initcli import '' && - completes $(compgen -d) && - - initcli import --copy -P '' && - completes $(compgen -d) && - - initcli import --log '' && - completes $(compgen -d) && - true -} - - -test_import_options() { - initcli imp - - completes \ - -h --help \ - -c --copy -C --nocopy \ - -w --write -W --nowrite \ - -a --autotag -A --noautotag \ - -p --resume -P --noresume \ - -l --log --flat -} - - -test_list_options() { - initcli list - - completes \ - -h --help \ - -a --album \ - -p --path -} - -test_list_query() { - initcli list 'x' && - [[ -z "${COMPREPLY[@]}" ]] && - - initcli list 'art' && - completes \ - 'artist:' \ - 'artpath:' && - - initcli list 'artits:x' && - [[ -z "${COMPREPLY[@]}" ]] && - true -} - -test_help_command() { - initcli help '' && - completes $COMMANDS && - true -} - -test_plugin_command() { - initcli te && - completes test && - - initcli test - && - completes -o --option && - true -} - -run_tests() { - local tests=$(set | \ - grep --extended-regexp --only-matching '^test_[a-zA-Z_]* \(\) $' |\ - grep --extended-regexp --only-matching '[a-zA-Z_]*' - ) - local fail=0 - - if [[ -n $@ ]]; then - tests="$@" - fi - - for t in $tests; do - $t || { fail=1 && echo "$t failed" >&2; } - done - return $fail -} - -run_tests "$@" && echo "completion tests passed" diff --git a/test/test_ui.py b/test/test_ui.py index 31f3f37da..5716f2f0b 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -20,7 +20,6 @@ from __future__ import division, absolute_import, print_function import os import shutil import re -import subprocess import platform from copy import deepcopy import six @@ -28,7 +27,7 @@ import six from mock import patch, Mock from test import _common from test._common import unittest -from test.helper import capture_stdout, has_program, TestHelper, control_stdin +from test.helper import capture_stdout, TestHelper, control_stdin from beets import library from beets import ui @@ -1110,56 +1109,6 @@ class PluginTest(_common.TestCase, TestHelper): self.run_command('test', lib=None) -@_common.slow_test() -class CompletionTest(_common.TestCase, TestHelper): - def test_completion(self): - # Load plugin commands - config['pluginpath'] = [_common.PLUGINPATH] - config['plugins'] = ['test'] - - # Do not load any other bash completion scripts on the system. - env = dict(os.environ) - env['BASH_COMPLETION_DIR'] = os.devnull - env['BASH_COMPLETION_COMPAT_DIR'] = os.devnull - - # Open a `bash` process to run the tests in. We'll pipe in bash - # commands via stdin. - cmd = os.environ.get('BEETS_TEST_SHELL', '/bin/bash --norc').split() - if not has_program(cmd[0]): - self.skipTest(u'bash not available') - tester = subprocess.Popen(cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, env=env) - - # Load bash_completion library. - for path in commands.BASH_COMPLETION_PATHS: - if os.path.exists(util.syspath(path)): - bash_completion = path - break - else: - self.skipTest(u'bash-completion script not found') - try: - with open(util.syspath(bash_completion), 'rb') as f: - tester.stdin.writelines(f) - except IOError: - self.skipTest(u'could not read bash-completion script') - - # Load completion script. - self.io.install() - self.run_command('completion', lib=None) - completion_script = self.io.getoutput().encode('utf-8') - self.io.restore() - tester.stdin.writelines(completion_script.splitlines(True)) - - # Load test suite. - test_script_name = os.path.join(_common.RSRC, b'test_completion.sh') - with open(test_script_name, 'rb') as test_script_file: - tester.stdin.writelines(test_script_file) - out, err = tester.communicate() - if tester.returncode != 0 or out != b'completion tests passed\n': - print(out.decode('utf-8')) - self.fail(u'test/test_completion.sh did not execute properly') - - class CommonOptionsParserCliTest(unittest.TestCase, TestHelper): """Test CommonOptionsParser and formatting LibModel formatting on 'list' command.