Merge branch 'completion'

Conflicts:
	beets/ui/commands.py
	docs/reference/cli.rst
This commit is contained in:
Thomas Scholtes 2014-03-02 23:06:05 +01:00
commit 8a5a2fcebf
7 changed files with 526 additions and 3 deletions

View file

@ -13,6 +13,8 @@ install:
- travis_retry pip install pylast flask
# unittest backport on Python < 2.7
- "[[ $TRAVIS_PYTHON_VERSION == '2.6' ]] && pip install unittest2 || true"
- sudo apt-get update
- sudo apt-get install -qq bash-completion
script: nosetests --with-coverage --cover-package=beets

View file

@ -39,6 +39,7 @@ from beets.util.functemplate import Template
from beets.util.confit import ConfigTypeError
from beets import library
from beets import config
from beets.util.confit import _package_path
# Global logger.
log = logging.getLogger('beets')
@ -1310,3 +1311,93 @@ def config_func(lib, opts, args):
config_cmd.func = config_func
default_commands.append(config_cmd)
# completion: print completion script
completion_cmd = ui.Subcommand('completion',
help='print shell script that provides command line completion')
def print_completion(*args):
for line in completion_script(default_commands + plugins.commands()):
print(line, end='')
if not (os.path.isfile(u'/etc/bash_completion') or
os.path.isfile(u'/usr/share/bash-completion/bash_completion') or
os.path.isfile(u'/usr/share/local/bash-completion/bash_completion')):
log.warn(u'Warning: Unable to find the bash-completion package. '
u'Command line completion might not work.')
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 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:
aliases[alias] = name
options[name] = {'flags': [], 'opts': []}
for opts in cmd.parser._get_all_options()[1:]:
if opts.action in ('store_true', 'store_false'):
option_type = 'flags'
else:
option_type = 'opts'
options[name][option_type].extend(opts._short_opts + opts._long_opts)
# Add global options
options['_global'] = {
'flags': ['-v', '--verbose'],
'opts': '-l --library -c --config -d --directory -h --help'.split(' ')
}
# Help subcommand
command_names.append('help')
# Add flags common to all commands
options['_common'] = {
'flags': ['-h', '--help']
}
# Start generating the script
yield "_beet() {\n"
# Command names
yield " local commands='%s'\n" % ' '.join(command_names)
yield "\n"
# Command aliases
yield " local aliases='%s'\n" % ' '.join(aliases.keys())
for alias, cmd in aliases.items():
yield " local alias__%s=%s\n" % (alias, cmd)
yield '\n'
# Fields
yield " fields='%s'\n" % ' '.join(
set(library.ITEM_KEYS + library.ALBUM_KEYS))
# Command options
for cmd, opts in options.items():
for option_type, option_list in opts.items():
if option_list:
option_list = ' '.join(option_list)
yield " local %s__%s='%s'\n" % (option_type, cmd, option_list)
yield ' _beet_dispatch\n'
yield '}\n'
completion_cmd.func = print_completion
default_commands.append(completion_cmd)

162
beets/ui/completion_base.sh Normal file
View file

@ -0,0 +1,162 @@
# 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

@ -17,9 +17,12 @@ Command-Line Interface
beet COMMAND [ARGS...]
The rest of this document describes the available commands. If you ever need
a quick list of what's available, just type ``beet help`` or ``beet help
COMMAND`` for help with a specific command.
Beets also offers command line completion via the `completion`_
command. The rest of this document describes the available
commands. If you ever need a quick list of what's available, just
type ``beet help`` or ``beet help COMMAND`` for help with a specific
command.
Commands
--------
@ -326,6 +329,41 @@ Show or edit the user configuration. This command does one of three things:
fallback option depending on your platform: ``open`` on OS X, ``xdg-open``
on Unix, and direct invocation on Windows.
completion
``````````
::
beet completion
Print a shell script that enables command line completion.
The script completes the names of subcommands and (after typing
``-``) options of the given command. If you are using a command that
accepts a query, the script will also complete field names. ::
beet list ar[TAB]
# artist: artist_credit: artist_sort: artpath:
beet list artp[TAB]
beet list artpat\:
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.
Note that completion of plugin commands only works for those plugins
that were enabled when running ``beet completion``. If you add a plugin
later on you might want to re-generate the script.
To enable completion in your current shell, type ``eval "$(beet
completion)"``. If you want to use it permanently, load the script from
your shell's rc-file.
Completion is only tested to work on Bash 3.2 and newer. It also
requires the ``bash-completion`` package which is available OSX (through
*homebrew* or *ports*) and Linuxes.
.. _global-flags:
Global Flags

View file

@ -9,6 +9,10 @@ class TestPlugin(BeetsPlugin):
def commands(self):
test = ui.Subcommand('test')
test.func = lambda *args: None
# Used in CompletionTest
test.parser.add_option('-o', '--option', dest='my_opt')
plugin = ui.Subcommand('plugin')
plugin.func = lambda *args: None
return [test, plugin]

185
test/test_completion.sh Normal file
View file

@ -0,0 +1,185 @@
# 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

@ -17,6 +17,7 @@
import os
import shutil
import re
import subprocess
import _common
from _common import unittest
@ -850,6 +851,46 @@ class PluginTest(_common.TestCase):
config['plugins'] = ['test']
ui._raw_main(['test'])
class CompletionTest(_common.TestCase):
def test_completion(self):
# Load plugin commands
config['pluginpath'] = [os.path.join(_common.RSRC, 'beetsplug')]
config['plugins'] = ['test']
test_script = os.path.join(os.path.dirname(__file__),
'test_completion.sh')
bash_completion = os.path.abspath(os.environ.get(
'BASH_COMPLETION_SCRIPT', '/etc/bash_completion'))
# Tests run in bash
shell = os.environ.get('BEETS_TEST_SHELL', '/bin/bash --norc')
tester = subprocess.Popen(shell.split(' '), stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
# Load bash_completion
with open(bash_completion, 'r') as bash_completion:
tester.stdin.writelines(bash_completion)
# Load complection script
self.io.install()
ui._raw_main(['completion'])
completion_script = self.io.getoutput()
self.io.restore()
tester.stdin.writelines(completion_script)
# from beets import plugins
# for cmd in plugins.commands():
# print(cmd.name)
# Load testsuite
with open(test_script, 'r') as test_script:
tester.stdin.writelines(test_script)
(out, err) = tester.communicate()
if tester.returncode != 0 or out != "completion tests passed\n":
print(out)
self.fail('test/test_completion.sh did not execute properly')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)