From 14ece207c9108b218c9199e3c64b7726292684ef Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 20 Feb 2014 01:15:03 +0100 Subject: [PATCH 01/18] Add `completion` command The command prints a shell script that provides completion for the `beet` command. To test it run `eval "$(beet completion)"` in your shell. I also included some crude testing for this. The `test/test_completion.sh` script runs tests in a shell and exit with a non-zero status code if the tests fail. It assumes that the completion script is already loaded in the executing shell. As of now the completion only works for bash 4.1 and newer. --- beets/completion.py | 65 ++++++++++++++++ beets/completion_base.sh | 143 +++++++++++++++++++++++++++++++++++ beets/ui/commands.py | 10 +++ test/test_completion.sh | 159 +++++++++++++++++++++++++++++++++++++++ test/test_ui.py | 26 +++++++ 5 files changed, 403 insertions(+) create mode 100644 beets/completion.py create mode 100644 beets/completion_base.sh create mode 100644 test/test_completion.sh diff --git a/beets/completion.py b/beets/completion.py new file mode 100644 index 000000000..3f73bdffc --- /dev/null +++ b/beets/completion.py @@ -0,0 +1,65 @@ +from __future__ import print_function +import os.path + +def completion_script(commands): + base_script = os.path.join(os.path.dirname(__file__), 'completion_base.sh') + for line in open(base_script, 'r'): + yield line + + 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 + aliases['?'] = 'help' + command_names.append('help') + + # Add flags common to all commands + options['_common'] = { + 'flags': ['-h', '--help'] + } + + # Start generating the script + yield "_beet_setup() {\n" + yield " commands='%s'\n" % ' '.join(command_names) + yield '\n' + + for alias, cmd in aliases.items(): + yield(" aliases['%s']='%s'\n" % (alias, cmd)) + yield '\n' + + for cmd, opts in options.items(): + for option_type, option_list in opts.items(): + if option_list: + option_list = ' '.join(option_list) + yield " %s[%s]='%s'\n" % (option_type, cmd, option_list) + yield '}\n' + + +if __name__ == "__main__": + from beets.ui.commands import default_commands + for line in generate_completion(default_commands): + print(line, end='') diff --git a/beets/completion_base.sh b/beets/completion_base.sh new file mode 100644 index 000000000..05364c9c5 --- /dev/null +++ b/beets/completion_base.sh @@ -0,0 +1,143 @@ +# 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, global options, and +# subcommand options. +# +# 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 only works for builtin commands and *not* for +# commands provided by plugins +# +# +# TODO +# ---- +# +# * Complete arguments for the `--format` option by expanding field variables. +# +# beet ls -f "$tit[TAB] +# beet ls -f "$title +# +# * Complete queries. +# +# beet ls art[TAB] +# beet ls artist: +# +# * Complete plugin commands by dynamically checking which commands are +# available. +# +# * Support long options with `=`, e.g. `--config=file`. Debian's bash +# completion package can handle this. +# + + +# Main entry point for completion +_beet() { + local cur prev commands + local -A flags opts aliases + + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + _beet_setup + + # Look for the beets subcommand + local arg cmd= + 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 && -n ${aliases[$cmd]} ]]; then + cmd=${aliases[$cmd]} + fi + + case $cmd in + "") + _beet_complete_global + ;; + help) + COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) + ;; + *) + _beet_complete $cmd + ;; + esac +} +complete -o filenames -F _beet beet + + +# Adds option and file completion to COMPREPLY for the subcommand $1 +_beet_complete() { + if [[ $cur == -* ]]; then + local completions="${flags[_common]} ${opts[$1]} ${flags[$1]}" + COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) + else + COMPREPLY+=( $(compgen -f -- $cur) ) + fi +} + + +# Add global options and commands 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 + COMPREPLY+=( $(compgen -f $cur)) + return + ;; + -d|--directory) + # Directory completion + COMPREPLY+=( $(compgen -d $cur)) + return + ;; + esac + + if [[ $cur == -* ]]; then + local completions="${opts[_global]} ${flags[_global]}" + COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) + elif [[ -n $cur && -n "${aliases[$cur]}" ]]; then + COMPREPLY+=( ${aliases[$cur]} ) + else + COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) + 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_setup function. This +# function sets the variables $flags, $opts, $commands, and $aliases. diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 88c09a010..9572dcb70 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -37,6 +37,7 @@ from beets.util import syspath, normpath, ancestry, displayable_path from beets.util.functemplate import Template from beets import library from beets import config +from beets.completion import completion_script # Global logger. log = logging.getLogger('beets') @@ -1286,3 +1287,12 @@ def write_func(lib, opts, args): write_items(lib, decargs(args), opts.pretend) write_cmd.func = write_func default_commands.append(write_cmd) + + +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): + print(line, end='') +completion_cmd.func = print_completion +default_commands.append(completion_cmd) diff --git a/test/test_completion.sh b/test/test_completion.sh new file mode 100644 index 000000000..4c27a05c5 --- /dev/null +++ b/test/test_completion.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +initcli() { + COMP_WORDS=( "beet" "$@" ) + let COMP_CWORD=${#COMP_WORDS[@]}-1 + _beet +} + +completes() { + for word in "$@"; do + [[ ${COMPREPLY[@]} =~ "$word" ]] || 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() { + initcli --library '' && + completes $(compgen -f) && + + initcli -l '' && + completes $(compgen -f) && + + initcli --config '' && + completes $(compgen -f) && + + initcli -c '' && + completes $(compgen -f) && + 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 -f) && + true +} + + +test_import_files() { + initcli import '' && + completes $(compgen -f) && + + initcli import --copy -P '' && + completes $(compgen -f) && + + initcli import --log '' && + completes $(compgen -f) && + 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_help_command() { + initcli help '' && + completes $COMMANDS && + + initcli '?' '' && + completes $COMMANDS && + 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 e5cbf95a8..0600824a4 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -17,6 +17,7 @@ import os import shutil import re +import subprocess import _common from _common import unittest @@ -741,6 +742,31 @@ class PluginTest(_common.TestCase): config['plugins'] = ['test'] ui._raw_main(['test']) + +class CompletionTest(_common.TestCase): + + def test_completion(self): + test_script = os.path.join(os.path.dirname(__file__), + 'test_completion.sh') + + # Tests run in bash + tester = subprocess.Popen('/bin/bash', stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + # Load complection script + self.io.install() + ui._raw_main(['completion']) + completion_script = self.io.getoutput() + self.io.restore() + tester.stdin.writelines(completion_script) + + # Load testsuite + with open(test_script, 'r') as test_script: + tester.stdin.writelines(test_script) + (out, err) = tester.communicate() + self.assertEqual(tester.returncode, 0) + self.assertEqual(out, "completion tests passed\n") + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From c1de721ca17525f6fa2fc8d4d5e12e23383571d3 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 20 Feb 2014 13:43:48 +0100 Subject: [PATCH 02/18] Remove unused code --- beets/completion.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/beets/completion.py b/beets/completion.py index 3f73bdffc..341a10914 100644 --- a/beets/completion.py +++ b/beets/completion.py @@ -1,4 +1,3 @@ -from __future__ import print_function import os.path def completion_script(commands): @@ -57,9 +56,3 @@ def completion_script(commands): option_list = ' '.join(option_list) yield " %s[%s]='%s'\n" % (option_type, cmd, option_list) yield '}\n' - - -if __name__ == "__main__": - from beets.ui.commands import default_commands - for line in generate_completion(default_commands): - print(line, end='') From c5d3483fc3c5d5bd7934d25e51f16b08e8307a68 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 20 Feb 2014 13:46:21 +0100 Subject: [PATCH 03/18] Add note for shell support --- beets/completion_base.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beets/completion_base.sh b/beets/completion_base.sh index 05364c9c5..accfab5d9 100644 --- a/beets/completion_base.sh +++ b/beets/completion_base.sh @@ -27,8 +27,9 @@ # completed if '-' has already been typed on the command line. # # Note that completion only works for builtin commands and *not* for -# commands provided by plugins +# commands provided by plugins. # +# Currently, only Bash 4.1 and newer is supported. # # TODO # ---- @@ -48,6 +49,8 @@ # # * Support long options with `=`, e.g. `--config=file`. Debian's bash # completion package can handle this. +# +# * Support for Bash 3.2 # From e8e0682aaea2d5ee142b2dc7f71706428155e652 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 22 Feb 2014 17:59:23 +0100 Subject: [PATCH 04/18] Add completion support for bash 3.2 Bash 3.2 does not have associative arrays, so we hack around that by using generic varibale names like `opts__$cmd`. We also don't support the "?" alias anymore. --- beets/completion.py | 13 +++++++------ beets/completion_base.sh | 40 +++++++++++++++++++++------------------- test/test_completion.sh | 5 +---- test/test_ui.py | 3 ++- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/beets/completion.py b/beets/completion.py index 341a10914..e06e52eda 100644 --- a/beets/completion.py +++ b/beets/completion.py @@ -33,7 +33,6 @@ def completion_script(commands): } # Help subcommand - aliases['?'] = 'help' command_names.append('help') # Add flags common to all commands @@ -42,17 +41,19 @@ def completion_script(commands): } # Start generating the script - yield "_beet_setup() {\n" - yield " commands='%s'\n" % ' '.join(command_names) - yield '\n' + yield "_beet() {\n" + yield " local commands='%s'\n" % ' '.join(command_names) + yield "\n" + yield " local aliases='%s'\n" % ' '.join(aliases.keys()) for alias, cmd in aliases.items(): - yield(" aliases['%s']='%s'\n" % (alias, cmd)) + yield " local alias__%s=%s\n" % (alias, cmd) yield '\n' for cmd, opts in options.items(): for option_type, option_list in opts.items(): if option_list: option_list = ' '.join(option_list) - yield " %s[%s]='%s'\n" % (option_type, cmd, option_list) + yield " local %s__%s='%s'\n" % (option_type, cmd, option_list) + yield ' _beet_dispatch\n' yield '}\n' diff --git a/beets/completion_base.sh b/beets/completion_base.sh index accfab5d9..a50371273 100644 --- a/beets/completion_base.sh +++ b/beets/completion_base.sh @@ -29,7 +29,7 @@ # Note that completion only works for builtin commands and *not* for # commands provided by plugins. # -# Currently, only Bash 4.1 and newer is supported. +# Currently, only Bash 3.2 and newer is supported. # # TODO # ---- @@ -50,25 +50,22 @@ # * Support long options with `=`, e.g. `--config=file`. Debian's bash # completion package can handle this. # -# * Support for Bash 3.2 -# -# Main entry point for completion -_beet() { - local cur prev commands - local -A flags opts aliases +# Determines the beets subcommand and dispatches the completion +# accordingly. +_beet_dispatch() { + local cur prev COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - _beet_setup # Look for the beets subcommand local arg cmd= for (( i=1; i < COMP_CWORD; i++ )); do arg="${COMP_WORDS[i]}" - if _list_include_item "${opts[_global]}" $arg; then + if _list_include_item "${opts___global}" $arg; then ((i++)) elif [[ "$arg" != -* ]]; then cmd="$arg" @@ -77,8 +74,8 @@ _beet() { done # Replace command shortcuts - if [[ -n $cmd && -n ${aliases[$cmd]} ]]; then - cmd=${aliases[$cmd]} + if [[ -n $cmd ]] && _list_include_item "$aliases" "$cmd"; then + eval "cmd=\$alias__$cmd" fi case $cmd in @@ -93,13 +90,15 @@ _beet() { ;; esac } -complete -o filenames -F _beet beet # Adds option and file completion to COMPREPLY for the subcommand $1 _beet_complete() { if [[ $cur == -* ]]; then - local completions="${flags[_common]} ${opts[$1]} ${flags[$1]}" + local opts flags completions + eval "opts=\$opts__$1" + eval "flags=\$flags__$1" + completions="${flags___common} ${opts} ${flags}" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) else COMPREPLY+=( $(compgen -f -- $cur) ) @@ -107,7 +106,7 @@ _beet_complete() { } -# Add global options and commands to the completion +# Add global options and subcommands to the completion _beet_complete_global() { case $prev in -h|--help) @@ -128,10 +127,12 @@ _beet_complete_global() { esac if [[ $cur == -* ]]; then - local completions="${opts[_global]} ${flags[_global]}" + local completions="$opts___global $flags___global" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) - elif [[ -n $cur && -n "${aliases[$cur]}" ]]; then - COMPREPLY+=( ${aliases[$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 @@ -139,8 +140,9 @@ _beet_complete_global() { # Returns true if the space separated list $1 includes $2 _list_include_item() { - [[ $1 =~ (^|[[:space:]])"$2"($|[[:space:]]) ]] + [[ " $1 " == *[[:space:]]$2[[:space:]]* ]] } -# This is where beets dynamically adds the _beet_setup function. This +# 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/test/test_completion.sh b/test/test_completion.sh index 4c27a05c5..2580ebed0 100644 --- a/test/test_completion.sh +++ b/test/test_completion.sh @@ -8,7 +8,7 @@ initcli() { completes() { for word in "$@"; do - [[ ${COMPREPLY[@]} =~ "$word" ]] || return 1 + [[ " ${COMPREPLY[@]} " == *[[:space:]]$word[[:space:]]* ]] || return 1 done } @@ -133,9 +133,6 @@ test_list_options() { test_help_command() { initcli help '' && completes $COMMANDS && - - initcli '?' '' && - completes $COMMANDS && true } diff --git a/test/test_ui.py b/test/test_ui.py index 0600824a4..237daa3c0 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -750,7 +750,8 @@ class CompletionTest(_common.TestCase): 'test_completion.sh') # Tests run in bash - tester = subprocess.Popen('/bin/bash', stdin=subprocess.PIPE, + shell = os.environ.get('BEETS_TEST_SHELL', '/bin/bash') + tester = subprocess.Popen(shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE) # Load complection script From fa6f7622e06e22a3cd0693a9e6bf72cc56f0e52b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 23 Feb 2014 22:16:59 +0100 Subject: [PATCH 05/18] Move completion into beets.ui package and use pkg_resources --- beets/ui/commands.py | 2 +- beets/{ => ui}/completion.py | 6 ++---- beets/{ => ui}/completion_base.sh | 0 3 files changed, 3 insertions(+), 5 deletions(-) rename beets/{ => ui}/completion.py (90%) rename beets/{ => ui}/completion_base.sh (100%) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 9572dcb70..42b06c5d5 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -37,7 +37,7 @@ from beets.util import syspath, normpath, ancestry, displayable_path from beets.util.functemplate import Template from beets import library from beets import config -from beets.completion import completion_script +from beets.ui.completion import completion_script # Global logger. log = logging.getLogger('beets') diff --git a/beets/completion.py b/beets/ui/completion.py similarity index 90% rename from beets/completion.py rename to beets/ui/completion.py index e06e52eda..8714b4a44 100644 --- a/beets/completion.py +++ b/beets/ui/completion.py @@ -1,9 +1,7 @@ -import os.path +from pkg_resources import resource_string def completion_script(commands): - base_script = os.path.join(os.path.dirname(__file__), 'completion_base.sh') - for line in open(base_script, 'r'): - yield line + yield resource_string(__name__, 'completion_base.sh') options = {} aliases = {} diff --git a/beets/completion_base.sh b/beets/ui/completion_base.sh similarity index 100% rename from beets/completion_base.sh rename to beets/ui/completion_base.sh From 4c00e52455ab54502cae017192c6e84dd293b89b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 23 Feb 2014 23:17:49 +0100 Subject: [PATCH 06/18] Completion for filenames with spaces --- beets/ui/completion_base.sh | 65 +++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh index a50371273..bcc066041 100644 --- a/beets/ui/completion_base.sh +++ b/beets/ui/completion_base.sh @@ -34,6 +34,8 @@ # 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] @@ -101,7 +103,7 @@ _beet_complete() { completions="${flags___common} ${opts} ${flags}" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) else - COMPREPLY+=( $(compgen -f -- $cur) ) + _beet_complete_filedir fi } @@ -116,12 +118,12 @@ _beet_complete_global() { ;; -l|--library|-c|--config) # Filename completion - COMPREPLY+=( $(compgen -f $cur)) + _beet_complete_filedir return ;; -d|--directory) # Directory completion - COMPREPLY+=( $(compgen -d $cur)) + _beet_complete_filedir -d return ;; esac @@ -138,6 +140,63 @@ _beet_complete_global() { fi } +# This function performs file and directory completion. It's better than +# simply using 'compgen -f', because it honours spaces in filenames. +# @param $1 If `-d', complete only on directories. Otherwise filter/pick only +# completions with `.$1' and the uppercase version of it as file +# extension. +# +# This function is based on code from debian's bash-completion package. +# +# Copyright the Bash Completion Maintainers +# +# +_beet_complete_filedir() { + local IFS=$'\n' + local tmp quoted_cur opt="$1" + + _beet_quote_for_readline "$cur" quoted_cur + + if [[ "$opt" != -d ]]; then + opt=-f + fi + + while read -r tmp; do + COMPREPLY+=( "$tmp" ) + done <<< "$(compgen "$opt" -- "$quoted_cur")" +} + +# This function quotes the argument in a way so that readline dequoting +# results in the original argument. This is necessary for at least +# `compgen' which requires its arguments quoted/escaped: +# $ ls "a'b/" +# c +# $ compgen -f "a'b/" # Wrong, doesn't return output +# $ compgen -f "a\'b/" # Good +# a\'b/c +# +# @param $1 Argument to quote +# @param $2 Name of variable to return result to +# +# This function was copied from debian's bash-completion package. +# +# Copyright the Bash Completion Maintainers +# +# +_beet_quote_for_readline() +{ + if [[ $1 == \'* ]]; then + # Leave out first character + printf -v $2 %s "${1:1}" + elif [[ -z $1 ]]; then + # Do not quote the empty string + printf -v $2 %s "$1" + else + printf -v $2 %q "$1" + fi + [[ ${!2} == \$* ]] && eval $2=${!2} +} + # Returns true if the space separated list $1 includes $2 _list_include_item() { [[ " $1 " == *[[:space:]]$2[[:space:]]* ]] From 81a28198aa4e7880255d6ef6b660f815028a55e2 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 27 Feb 2014 15:09:02 +0100 Subject: [PATCH 07/18] Test completion with clean bash instance --- test/test_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index 237daa3c0..efe0f0dee 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -750,8 +750,8 @@ class CompletionTest(_common.TestCase): 'test_completion.sh') # Tests run in bash - shell = os.environ.get('BEETS_TEST_SHELL', '/bin/bash') - tester = subprocess.Popen(shell, stdin=subprocess.PIPE, + shell = os.environ.get('BEETS_TEST_SHELL', '/bin/bash --norc') + tester = subprocess.Popen(shell.split(' '), stdin=subprocess.PIPE, stdout=subprocess.PIPE) # Load complection script From 211d3ac1cb04144b558a9c057c2c0ad452a79f69 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 27 Feb 2014 15:28:16 +0100 Subject: [PATCH 08/18] Do not evaluate in subshell --- test/test_completion.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_completion.sh b/test/test_completion.sh index 2580ebed0..442d77810 100644 --- a/test/test_completion.sh +++ b/test/test_completion.sh @@ -1,5 +1,7 @@ #!/bin/bash +. /etc/bash_completion + initcli() { COMP_WORDS=( "beet" "$@" ) let COMP_CWORD=${#COMP_WORDS[@]}-1 @@ -42,7 +44,7 @@ test_command_aliases() { completes list && initcli l && - !( completes ls; ) && + ! completes ls && initcli im && completes import && From 996a1d6c906cd90ae9ad5cb2faed5633efcc8987 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 27 Feb 2014 17:52:23 +0100 Subject: [PATCH 09/18] Use bash-completion package --- beets/ui/completion_base.sh | 66 +++---------------------------------- test/test_completion.sh | 25 +++++++------- test/test_ui.py | 10 ++++-- 3 files changed, 25 insertions(+), 76 deletions(-) diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh index bcc066041..23887e715 100644 --- a/beets/ui/completion_base.sh +++ b/beets/ui/completion_base.sh @@ -60,8 +60,7 @@ _beet_dispatch() { local cur prev COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" + _get_comp_words_by_ref cur prev # Look for the beets subcommand local arg cmd= @@ -103,7 +102,7 @@ _beet_complete() { completions="${flags___common} ${opts} ${flags}" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) else - _beet_complete_filedir + _filedir fi } @@ -118,12 +117,12 @@ _beet_complete_global() { ;; -l|--library|-c|--config) # Filename completion - _beet_complete_filedir + _filedir return ;; -d|--directory) # Directory completion - _beet_complete_filedir -d + _filedir -d return ;; esac @@ -140,63 +139,6 @@ _beet_complete_global() { fi } -# This function performs file and directory completion. It's better than -# simply using 'compgen -f', because it honours spaces in filenames. -# @param $1 If `-d', complete only on directories. Otherwise filter/pick only -# completions with `.$1' and the uppercase version of it as file -# extension. -# -# This function is based on code from debian's bash-completion package. -# -# Copyright the Bash Completion Maintainers -# -# -_beet_complete_filedir() { - local IFS=$'\n' - local tmp quoted_cur opt="$1" - - _beet_quote_for_readline "$cur" quoted_cur - - if [[ "$opt" != -d ]]; then - opt=-f - fi - - while read -r tmp; do - COMPREPLY+=( "$tmp" ) - done <<< "$(compgen "$opt" -- "$quoted_cur")" -} - -# This function quotes the argument in a way so that readline dequoting -# results in the original argument. This is necessary for at least -# `compgen' which requires its arguments quoted/escaped: -# $ ls "a'b/" -# c -# $ compgen -f "a'b/" # Wrong, doesn't return output -# $ compgen -f "a\'b/" # Good -# a\'b/c -# -# @param $1 Argument to quote -# @param $2 Name of variable to return result to -# -# This function was copied from debian's bash-completion package. -# -# Copyright the Bash Completion Maintainers -# -# -_beet_quote_for_readline() -{ - if [[ $1 == \'* ]]; then - # Leave out first character - printf -v $2 %s "${1:1}" - elif [[ -z $1 ]]; then - # Do not quote the empty string - printf -v $2 %s "$1" - else - printf -v $2 %q "$1" - fi - [[ ${!2} == \$* ]] && eval $2=${!2} -} - # Returns true if the space separated list $1 includes $2 _list_include_item() { [[ " $1 " == *[[:space:]]$2[[:space:]]* ]] diff --git a/test/test_completion.sh b/test/test_completion.sh index 442d77810..6576adb04 100644 --- a/test/test_completion.sh +++ b/test/test_completion.sh @@ -1,10 +1,8 @@ -#!/bin/bash - -. /etc/bash_completion - initcli() { COMP_WORDS=( "beet" "$@" ) let COMP_CWORD=${#COMP_WORDS[@]}-1 + COMP_LINE="${COMP_WORDS[@]}" + let COMP_POINT=${#COMP_LINE} _beet } @@ -64,17 +62,20 @@ test_global_opts() { 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 -f) && + completes $(compgen -d) && initcli -l '' && - completes $(compgen -f) && + completes $(compgen -d) && initcli --config '' && - completes $(compgen -f) && + completes $(compgen -d) && initcli -c '' && - completes $(compgen -f) && + completes $(compgen -d) && true } @@ -94,20 +95,20 @@ test_fields_command() { completes -h --help && initcli fields '' && - completes $(compgen -f) && + completes $(compgen -d) && true } test_import_files() { initcli import '' && - completes $(compgen -f) && + completes $(compgen -d) && initcli import --copy -P '' && - completes $(compgen -f) && + completes $(compgen -d) && initcli import --log '' && - completes $(compgen -f) && + completes $(compgen -d) && true } diff --git a/test/test_ui.py b/test/test_ui.py index efe0f0dee..d8b34d529 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -748,12 +748,17 @@ class CompletionTest(_common.TestCase): def test_completion(self): test_script = os.path.join(os.path.dirname(__file__), 'test_completion.sh') + bash_completion = '/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']) @@ -765,8 +770,9 @@ class CompletionTest(_common.TestCase): with open(test_script, 'r') as test_script: tester.stdin.writelines(test_script) (out, err) = tester.communicate() - self.assertEqual(tester.returncode, 0) - self.assertEqual(out, "completion tests passed\n") + 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__) From 8450d51bab56be08651a7027b425433ea00101e7 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 27 Feb 2014 17:57:02 +0100 Subject: [PATCH 10/18] bash-completion package can be set by environment --- test/test_ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_ui.py b/test/test_ui.py index d8b34d529..b13fae80f 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -748,7 +748,8 @@ class CompletionTest(_common.TestCase): def test_completion(self): test_script = os.path.join(os.path.dirname(__file__), 'test_completion.sh') - bash_completion = '/etc/bash_completion' + 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') From 74cb897f79ff19a43eb100de16b700c1004067a3 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 27 Feb 2014 18:16:02 +0100 Subject: [PATCH 11/18] Add warning if completion package not found --- beets/ui/commands.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 42b06c5d5..fe03cc1a4 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1292,7 +1292,14 @@ default_commands.append(write_cmd) completion_cmd = ui.Subcommand('completion', help='print shell script that provides command line completion') def print_completion(*args): + 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.') + for line in completion_script(default_commands): print(line, end='') + completion_cmd.func = print_completion default_commands.append(completion_cmd) From 2b0929b71bf9460859ce37426b4c897351077072 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Thu, 27 Feb 2014 22:13:09 +0100 Subject: [PATCH 12/18] Complete queries --- beets/ui/completion.py | 10 +++++++++ beets/ui/completion_base.sh | 41 +++++++++++++++++++++++++++---------- test/test_completion.sh | 17 +++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/beets/ui/completion.py b/beets/ui/completion.py index 8714b4a44..9f4eb1bd6 100644 --- a/beets/ui/completion.py +++ b/beets/ui/completion.py @@ -1,4 +1,5 @@ from pkg_resources import resource_string +from beets import library def completion_script(commands): yield resource_string(__name__, 'completion_base.sh') @@ -40,18 +41,27 @@ def completion_script(commands): # 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' diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh index 23887e715..d297aa8a9 100644 --- a/beets/ui/completion_base.sh +++ b/beets/ui/completion_base.sh @@ -57,13 +57,13 @@ # Determines the beets subcommand and dispatches the completion # accordingly. _beet_dispatch() { - local cur prev + local cur prev cmd= COMPREPLY=() - _get_comp_words_by_ref cur prev + _get_comp_words_by_ref -n : cur prev # Look for the beets subcommand - local arg cmd= + local arg for (( i=1; i < COMP_CWORD; i++ )); do arg="${COMP_WORDS[i]}" if _list_include_item "${opts___global}" $arg; then @@ -80,25 +80,28 @@ _beet_dispatch() { fi case $cmd in - "") - _beet_complete_global - ;; help) COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) ;; + list|remove|move|update|write|stats) + _beet_complete_query + ;; + "") + _beet_complete_global + ;; *) - _beet_complete $cmd + _beet_complete ;; esac } -# Adds option and file completion to COMPREPLY for the subcommand $1 +# Adds option and file completion to COMPREPLY for the subcommand $cmd _beet_complete() { if [[ $cur == -* ]]; then local opts flags completions - eval "opts=\$opts__$1" - eval "flags=\$flags__$1" + eval "opts=\$opts__$cmd" + eval "flags=\$flags__$cmd" completions="${flags___common} ${opts} ${flags}" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) else @@ -133,12 +136,28 @@ _beet_complete_global() { elif [[ -n $cur ]] && _list_include_item "$aliases" "$cur"; then local cmd eval "cmd=\$alias__$cur" - COMPREPLY+=( $cmd ) + 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:]]* ]] diff --git a/test/test_completion.sh b/test/test_completion.sh index 6576adb04..3ae745143 100644 --- a/test/test_completion.sh +++ b/test/test_completion.sh @@ -1,3 +1,6 @@ +# Function stub +compopt() { return 0; } + initcli() { COMP_WORDS=( "beet" "$@" ) let COMP_CWORD=${#COMP_WORDS[@]}-1 @@ -133,6 +136,20 @@ test_list_options() { -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 && From 57f7fccdde65d7a5d523ae4975a3d273f098fb97 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 28 Feb 2014 16:57:22 +0100 Subject: [PATCH 13/18] Install bash-completion on travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 095c04568..a2077b7b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 From 5c7104adb0a796825b78619abf4a9d7f42d258e0 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 28 Feb 2014 17:15:04 +0100 Subject: [PATCH 14/18] Print warning last --- beets/ui/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index fe03cc1a4..8a8b67f4c 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1292,14 +1292,14 @@ default_commands.append(write_cmd) 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): + 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.') - for line in completion_script(default_commands): - print(line, end='') completion_cmd.func = print_completion default_commands.append(completion_cmd) From 25080bd59fcd42811ac748fa2dce440ac7ebe89d Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 2 Mar 2014 15:50:23 +0100 Subject: [PATCH 15/18] Add documentation for completion command --- beets/ui/completion_base.sh | 9 ++------- docs/reference/cli.rst | 40 ++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh index d297aa8a9..1be927dac 100644 --- a/beets/ui/completion_base.sh +++ b/beets/ui/completion_base.sh @@ -17,8 +17,8 @@ # Completion for the `beet` command # ================================= # -# Load this script to complete beets subcommands, global options, and -# subcommand options. +# 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 @@ -41,11 +41,6 @@ # beet ls -f "$tit[TAB] # beet ls -f "$title # -# * Complete queries. -# -# beet ls art[TAB] -# beet ls artist: -# # * Complete plugin commands by dynamically checking which commands are # available. # diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index c44ec3a02..5d8947f64 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -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 -------- @@ -304,6 +307,37 @@ fields Show the item and album metadata fields available for use in :doc:`query` and :doc:`pathformat`. Includes any template fields provided by plugins. +completion +`````````` + +:: + + beet completion + +Print a shell script that enables command line completion. + +The script completes the names of builtin subcommands and (after typing +``-``) options of the given command. Currently, it does not support +plugin commands. 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. + +To enable completion in your current shell, run ``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 From 44c843793b0333b4ad262512e258292b830b0a5d Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 2 Mar 2014 16:18:16 +0100 Subject: [PATCH 16/18] Add completion of plugin commands --- beets/ui/commands.py | 2 +- beets/ui/completion_base.sh | 11 +++++------ docs/reference/cli.rst | 13 ++++++++----- test/rsrc/beetsplug/test.py | 4 ++++ test/test_completion.sh | 9 +++++++++ test/test_ui.py | 7 +++++++ 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 8a8b67f4c..a1bdf7b5b 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1292,7 +1292,7 @@ default_commands.append(write_cmd) 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): + 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 diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh index 1be927dac..2164914c0 100644 --- a/beets/ui/completion_base.sh +++ b/beets/ui/completion_base.sh @@ -26,10 +26,12 @@ # it also completes filenames or directories. Options are only # completed if '-' has already been typed on the command line. # -# Note that completion only works for builtin commands and *not* for -# commands provided by plugins. +# 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. +# Currently, only Bash 3.2 and newer is supported and the +# `bash-completion` package is requied. # # TODO # ---- @@ -41,9 +43,6 @@ # beet ls -f "$tit[TAB] # beet ls -f "$title # -# * Complete plugin commands by dynamically checking which commands are -# available. -# # * Support long options with `=`, e.g. `--config=file`. Debian's bash # completion package can handle this. # diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 5d8947f64..4376a5b03 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -316,10 +316,9 @@ completion Print a shell script that enables command line completion. -The script completes the names of builtin subcommands and (after typing -``-``) options of the given command. Currently, it does not support -plugin commands. If you are using a command that accepts a query, the -script will also complete field names. :: +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: @@ -329,7 +328,11 @@ 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. -To enable completion in your current shell, run ``eval "$(beet +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. diff --git a/test/rsrc/beetsplug/test.py b/test/rsrc/beetsplug/test.py index a1ad19441..792676b51 100644 --- a/test/rsrc/beetsplug/test.py +++ b/test/rsrc/beetsplug/test.py @@ -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] diff --git a/test/test_completion.sh b/test/test_completion.sh index 3ae745143..88a0aca9b 100644 --- a/test/test_completion.sh +++ b/test/test_completion.sh @@ -156,6 +156,15 @@ test_help_command() { 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_]* \(\) $' |\ diff --git a/test/test_ui.py b/test/test_ui.py index b13fae80f..1af9efb34 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -746,6 +746,10 @@ class PluginTest(_common.TestCase): 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( @@ -766,6 +770,9 @@ class CompletionTest(_common.TestCase): 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: From c51e541ae2f02b99aeb3b64152f42a883d89fadf Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 2 Mar 2014 19:59:35 +0100 Subject: [PATCH 17/18] Move completion_script into commands module --- beets/ui/commands.py | 71 ++++++++++++++++++++++++++++++++++++++++++ beets/ui/completion.py | 67 --------------------------------------- 2 files changed, 71 insertions(+), 67 deletions(-) delete mode 100644 beets/ui/completion.py diff --git a/beets/ui/commands.py b/beets/ui/commands.py index a1bdf7b5b..1bfc7d0d9 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -23,6 +23,7 @@ import time import itertools import codecs from datetime import datetime +from pkg_resources import resource_string import beets from beets import ui @@ -1300,6 +1301,76 @@ def print_completion(*args): 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. + """ + yield resource_string(__name__, 'completion_base.sh') + + 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) diff --git a/beets/ui/completion.py b/beets/ui/completion.py deleted file mode 100644 index 9f4eb1bd6..000000000 --- a/beets/ui/completion.py +++ /dev/null @@ -1,67 +0,0 @@ -from pkg_resources import resource_string -from beets import library - -def completion_script(commands): - yield resource_string(__name__, 'completion_base.sh') - - 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' From 210592a06b1b5bb5d67cb492506c7f2fd9ba8349 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 2 Mar 2014 21:11:57 +0100 Subject: [PATCH 18/18] Use _package path instead of resource_string --- beets/ui/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 1bfc7d0d9..1a19cf509 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -23,7 +23,6 @@ import time import itertools import codecs from datetime import datetime -from pkg_resources import resource_string import beets from beets import ui @@ -39,6 +38,7 @@ from beets.util.functemplate import Template from beets import library from beets import config from beets.ui.completion import completion_script +from beets.util.confit import _package_path # Global logger. log = logging.getLogger('beets') @@ -1307,7 +1307,9 @@ def completion_script(commands): ``commands`` is alist of ``ui.Subcommand`` instances to generate completion data for. """ - yield resource_string(__name__, 'completion_base.sh') + 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 = {}