[英]bash tab completion with spaces
当可能的选项可能包含空格时,我遇到了 bash-completion 问题。
假设我想要一个与第一个参数相呼应的函数:
function test1() {
echo $1
}
我生成了一个可能的完成选项列表(有些有空格,有些没有),但我没能正确处理空格。
function pink() {
# my real-world example generates a similar string using awk and other commands
echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}
function _test() {
cur=${COMP_WORDS[COMP_CWORD]}
use=`pink`
COMPREPLY=( $( compgen -W "$use" -- $cur ) )
}
complete -o filenames -F _test test
当我尝试这个时,我得到:
$ test <tab><tab>
david_gilmour nick roger waters
mason richard syd-barrett wright
$ test r<tab><tab>
richard roger waters wright
这显然不是我的意思。
如果我不为COMPREPLY
分配一个数组,即只有$( compgen -W "$use" -- $cur )
,如果只剩下一个选项,我就会让它工作:
$ test n<tab>
$ test nick\ mason <cursor>
但是如果还有几个选项,它们都用单引号括起来:
$ test r<tab><tab>
$ test 'roger waters
richard wright' <cursor>
我的COMPREPLY
变量一定有问题,但我不知道是什么......
(在 solaris 上运行 bash,以防万一……)
可能包含空格的自定义制表符完成词非常困难。 据我所知,没有优雅的解决方案。 也许某些未来版本的compgen
会产生一个数组,而不是一次输出一行的可能性,甚至接受来自数组的 wordlist 参数。 但在那之前,以下方法可能会有所帮助。
理解这个问题很重要,即( $(compgen ... ) )
是一个数组,它是通过在$IFS
中的字符处拆分compgen
命令的输出而生成的,默认情况下它是任何空白字符。 因此,如果compgen
返回:
roger waters
richard wright
那么COMPREPLY
将有效地设置为数组(roger waters richard wright)
,总共有四个可能的完成。 如果您改为使用( "$(compgen ...)")
,则COMPREPLY
将设置为数组($'roger waters\\nrichard wright')
,该数组只有一个可能的完成(完成内有一个换行符)。 这些都不是你想要的。
如果所有可能的compgen
没有换行符,那么您可以通过临时重置IFS
然后恢复它来安排在换行符处拆分compgen
返回。 但我认为更优雅的解决方案是只使用mapfile
:
_test () {
cur=${COMP_WORDS[COMP_CWORD]};
use=`pink`;
## See note at end of answer w.r.t. "$cur" ##
mapfile -t COMPREPLY < <( compgen -W "$use" -- "$cur" )
}
mapfile
命令将compgen
发送到stdout
放入数组COMPREPLY
。 ( -t
选项会导致从每一行中删除尾随的换行符,这几乎总是您使用mapfile
时想要的。有关更多选项,请参阅help mapfile
。)
这并没有解决问题的另一个烦人的部分,即将 wordlist 改造成compgen
可接受的compgen
。 由于compgen
不允许多个-W
选项,也不接受数组,因此唯一的选择是以bash
词(带引号和全部)将生成所需列表的方式格式化字符串。 实际上,这意味着手动添加转义符,就像您在函数pink
所做的那样:
pink() {
echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}
但这很容易发生事故而且很烦人。 更好的解决方案将允许直接指定替代方案,特别是如果以某种方式生成替代方案。 生成可能包含空格的替代项的一个好方法是将它们放入一个数组中。 给定一个数组,您可以充分利用printf
的%q
格式为compgen -W
生成正确引用的输入字符串:
# This is a proxy for a database query or some such which produces the alternatives
cat >/tmp/pink <<EOP
nick mason
syd-barrett
david_gilmour
roger waters
richard wright
EOP
# Generate an array with the alternatives
mapfile -t pink </tmp/pink
# Use printf to turn the array into a quoted string:
_test () {
mapfile -t COMPREPLY < <( compgen -W "$(printf '%q ' "${pink[@]}")" -- "$2" )
}
正如所写的那样,该完成功能不会以 bash 将其作为单个单词接受的形式输出完成。 换句话说,完成roger waters
生成为roger waters
而不是roger\\ waters
。 在目标是生成正确引用的补全(可能)的情况下,有必要在compgen
过滤compgen
全列表后compgen
添加转义:
_test () {
declare -a completions
mapfile -t completions < <( compgen -W "$(printf '%q ' "${pink[@]}")" -- "$2" )
local comp
COMPREPLY=()
for comp in "${completions[@]}"; do
COMPREPLY+=("$(printf "%q" "$comp")")
done
}
注意:我用$2
替换了$cur
的计算,因为通过complete -F
调用的函数将命令作为$1
传递,将要完成的单词作为$2
传递。 (它也将前一个单词作为$3
传递。)另外,引用它很重要,这样它就不会在进入compgen
过程中被分compgen
。
如果您需要处理字符串中的数据,您可以使用 Bash 的内置字符串替换运算符。
function _test() {
local iter use cur
cur=${COMP_WORDS[COMP_CWORD]}
use="nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
# swap out escaped spaces temporarily
use="${use//\\ /___}"
# split on all spaces
for iter in $use; do
# only reply with completions
if [[ $iter =~ ^$cur ]]; then
# swap back our escaped spaces
COMPREPLY+=( "${iter//___/ }" )
fi
done
}
好吧,这个疯狂的装置很大程度上借鉴了rici的解决方案,不仅完全有效,而且还引用了任何需要它的完成,而且只有那些。
pink() {
# simulating actual awk output
echo "nick mason"
echo "syd-barrett"
echo "david_gilmour"
echo "roger waters"
echo "richard wright"
}
_test() {
cur=${COMP_WORDS[COMP_CWORD]}
mapfile -t patterns < <( pink )
mapfile -t COMPREPLY < <( compgen -W "$( printf '%q ' "${patterns[@]}" )" -- "$cur" | awk '/ / { print "\""$0"\"" } /^[^ ]+$/ { print $0 }' )
}
complete -F _test test
因此,就我可以测试而言,它完全实现了ls
类的行为,减去了特定于路径的部分。
这是_test
函数的更详细版本,因此它变得更容易理解:
_test() {
local cur escapedPatterns
cur=${COMP_WORDS[COMP_CWORD]}
mapfile -t patterns < <( pink )
escapedPatterns="$( printf '%q ' "${patterns[@]}" )"
mapfile -t COMPREPLY < <( compgen -W "$escapedPatterns" -- "$cur" | quoteIfNeeded )
}
quoteIfNeeded() {
# Only if it contains spaces. Otherwise return as-is.
awk '/ / { print "\""$0"\"" } /^[^ ]+$/ { print $0 }'
}
这些甚至都没有针对效率进行远程优化。 再说一次,这只是选项卡完成,对于任何相当大的完成列表,它不会造成明显的延迟。
mapfile
将awk
输出拉入数组。%q
后面有一个空格作为分隔标记。$cur
,非常重要!compgen
的输出。 并且仅当它包含空格时。mapfile
调用将该输出输入 COMPREPLY。-o filenames
。它只适用于所有这些技巧。 即使缺少一个,它也会失败。 相信我; 我试过了。 ;)
我没有mapfile
可用。 以下似乎比其他答案更简单,对我来说效果很好:
_completion() {
local CUR=${COMP_WORDS[COMP_CWORD]}
local OPT
local -a OPTS
while read -r OPT; do
local OPT_ESC
OPT_ESC="$(printf '%q' "$OPT")"
[[ -z "$CUR" ]] || [[ "$OPT_ESC" == "$CUR"* ]] && \
COMPREPLY+=("$OPT_ESC")
done < "${TOKEN_FILE}"
}
TOKEN_FILE
中的每一行都是一个完成选项。<(token_generator)
替换"${TOKEN_FILE}"
,其中token_genorator
是一些命令而不是生成完成标记,一次在线。printf '%q'
为我们提供了一个 bash 转义字符串,适合在命令行上作为单个标记使用。 所以,如果TOKEN_FILE
是:
option a
another option
an option with many spaces
然后制表符完成将打印:
an\ option\ with\ many\ spaces another\ option
option\ a
OP 代码的问题只是compgen
输出的扩展。 它使用 shell 的草率扩展COMPREPLY=(...)
,它在每个空间都中断。
将该行替换为
mapfile -t COMPREPLY < <( compgen -W "$use" -- $cur )
并且代码工作正常。
请注意, Shellcheck会抱怨COMPREPLY=(...)
扩展,并会建议改用mapfile
(请参阅此处)。
另外请注意,您在建议的完成中看不到转义字符,但是当您按TAB
键时,shell 会将其添加到命令中。 如果您以引号(单引号或双引号)开始您的论点,shell 将使用引号(您选择的引号)而不是反斜杠来保护空格。 只要正确构建COMPREPLY
,这一切都是免费的,因此无需破坏完成模式。
正如其他人指出的那样,提供扩展模式的命令可以每行返回一个(例如awk
或ls
),而不是作为转义字符串。 正如您现在肯定知道的那样,您可以使用mapfile
将输出读入数组:
mapfile -t my_array < <(the_command)
问题是compgen
想要一个字符串而不是一个数组,所以你需要再次展开它,但你需要转义单个元素内的空格。 这可能是整个过程中唯一棘手的部分。
幸运的是,您可以同时进行数组扩展和子字符串替换:
"${my_array[*]// /\\ }"
您可以将结果直接提供给compgen
:
function _test() {
local use cur
cur=${COMP_WORDS[COMP_CWORD]}
mapfile -t use < <(pink)
mapfile -t COMPREPLY < <(compgen -W "${use[*]// /\\ }" -- "$cur")
}
免责声明:对于之前答案中已经存在的许多概念,我深表歉意。 我只是想强调将补全正确地拆分到COMPREPLY
数组中是 Tab 补全的唯一要求。 之后 shell 会处理所有事情,所以它毕竟不是那么困难。 而且,作为一种教学习惯,我更喜欢在提供参考方案之前解决OP方案中的问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.