繁体   English   中英

带空格的 bash tab 补全

[英]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 }'
}

这些甚至都没有针对效率进行远程优化。 再说一次,这只是选项卡完成,对于任何相当大的完成列表,它不会造成明显的延迟。

它的工作原理是:

  1. 使用mapfileawk输出拉入数组。
  2. 转义数组并将其放入字符串中。
  3. %q后面有一个空格作为分隔标记。
  4. 引用$cur ,非常重要!
  5. 引用compgen输出 并且仅当它包含空格时。
  6. 使用另一个mapfile调用将该输出输入 COMPREPLY。
  7. 使用-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 ,这一切都是免费的,因此无需破坏完成模式。


正如其他人指出的那样,提供扩展模式的命令可以每行返回一个(例如awkls ),而不是作为转义字符串。 正如您现在肯定知道的那样,您可以使用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM