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.
This commit is contained in:
Adrian Sampson 2016-09-11 00:16:56 -04:00 committed by Johnny Robeson
parent 944808016e
commit b762eed171
6 changed files with 4 additions and 519 deletions

View file

@ -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 *

View file

@ -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)

View file

@ -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

View file

@ -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
```

View file

@ -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"

View file

@ -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.