mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 21:14:19 +01:00
Merge branch 'completion'
Conflicts: beets/ui/commands.py docs/reference/cli.rst
This commit is contained in:
commit
8a5a2fcebf
7 changed files with 526 additions and 3 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
162
beets/ui/completion_base.sh
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
185
test/test_completion.sh
Normal 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"
|
||||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue