简体   繁体   English

带空格的 bash tab 补全

[英]bash tab completion with spaces

I'm having a problem with bash-completion when the possible options may contain spaces.当可能的选项可能包含空格时,我遇到了 bash-completion 问题。

Let's say I want a function which echoes the first argument:假设我想要一个与第一个参数相呼应的函数:

function test1() {
        echo $1
}

I generate a list of possible completion options (some have spaces, some not), but I don't manage to handle spaces correctly.我生成了一个可能的完成选项列表(有些有空格,有些没有),但我没能正确处理空格。

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

When I try this, I get:当我尝试这个时,我得到:

$ test <tab><tab>
david_gilmour  nick           roger          waters
mason          richard        syd-barrett    wright
$ test r<tab><tab>
richard  roger    waters   wright

which is obviously not what I meant.这显然不是我的意思。

If I don't assign an array to COMPREPLY , ie only $( compgen -W "$use" -- $cur ) , I get it working if only one option remains:如果我不为COMPREPLY分配一个数组,即只有$( compgen -W "$use" -- $cur ) ,如果只剩下一个选项,我就会让它工作:

$ test n<tab>
$ test nick\ mason <cursor>

But if several options remain, they are all printed within single quotes:但是如果还有几个选项,它们都用单引号括起来:

$ test r<tab><tab>
$ test 'roger waters
richard wright' <cursor>

There must be something wrong with my COMPREPLY variable, but I can't figure out what...我的COMPREPLY变量一定有问题,但我不知道是什么......

(running bash on solaris, in case that makes a difference...) (在 solaris 上运行 bash,以防万一……)

Custom tab-completing words which might include whitespace is annoyingly difficult.可能包含空格的自定义制表符完成词非常困难。 And as far as I know there is no elegant solution.据我所知,没有优雅的解决方案。 Perhaps some future version of compgen will be kind enough to produce an array rather than outputting possibilities one line at a time, and even accept the wordlist argument from an array.也许某些未来版本的compgen会产生一个数组,而不是一次输出一行的可能性,甚至接受来自数组的 wordlist 参数。 But until then, the following approach may help.但在那之前,以下方法可能会有所帮助。

It's important to understand the problem, which is that ( $(compgen ... ) ) is an array produced by splitting the output of the compgen command at the characters in $IFS , which by default is any whitespace character.理解这个问题很重要,即( $(compgen ... ) )是一个数组,它是通过在$IFS中的字符处拆分compgen命令的输出而生成的,默认情况下它是任何空白字符。 So if compgen returns:因此,如果compgen返回:

roger waters
richard wright

then COMPREPLY will effectively be set to the array (roger waters richard wright) , for a total of four possible completions.那么COMPREPLY将有效地设置为数组(roger waters richard wright) ,总共有四个可能的完成。 If you instead use ( "$(compgen ...)") , then COMPREPLY will be set to the array ($'roger waters\\nrichard wright') , which has only one possible completion (with a newline inside the completion).如果您改为使用( "$(compgen ...)") ,则COMPREPLY将设置为数组($'roger waters\\nrichard wright') ,该数组只有一个可能的完成(完成内有一个换行符)。 Neither of those are what you want.这些都不是你想要的。

If none of the possible completions has a newline character, then you could arrange for the compgen return to be split at the newline character by temporarily resetting IFS and then restoring it.如果所有可能的compgen没有换行符,那么您可以通过临时重置IFS然后恢复它来安排在换行符处拆分compgen返回。 But I think a more elegant solution is to just use mapfile :但我认为更优雅的解决方案是只使用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" )
}

The mapfile command places the lines sent by compgen to stdout into the array COMPREPLY . mapfile命令将compgen发送到stdout放入数组COMPREPLY (The -t option causes the trailing newline to be removed from each line, which is almost always what you want when you use mapfile . See help mapfile for more options.) -t选项会导致从每一行中删除尾随的换行符,这几乎总是您使用mapfile时想要的。有关更多选项,请参阅help mapfile 。)

This doesn't deal with the other annoying part of the problem, which is mangling the wordlist into a form acceptable by compgen .这并没有解决问题的另一个烦人的部分,即将 wordlist 改造成compgen可接受的compgen Since compgen does not allow multiple -W options, and nor does it accept an array, the only option is to format a string in a such a way that bash word-splitting (with quotes and all) would generate the desired list.由于compgen不允许多个-W选项,也不接受数组,因此唯一的选择是以bash词(带引号和全部)将生成所需列表的方式格式化字符串。 In effect, that means manually adding escapes, as you did in your function pink :实际上,这意味着手动添加转义符,就像您在函数pink所做的那样:

pink() {
    echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}

But that's accident-prone and annoying.但这很容易发生事故而且很烦人。 A nicer solution would allow the specification of the alternatives directly, particularly if the alternatives are being generated in some fashion.更好的解决方案将允许直接指定替代方案,特别是如果以某种方式生成替代方案。 A good way of generating alternatives which might include whitespace is to put them into an array.生成可能包含空格的替代项的一个好方法是将它们放入一个数组中。 Given an array, you can make good use of printf 's %q format to produce a properly-quoted input string for compgen -W :给定一个数组,您可以充分利用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" )
}

As written, that completion function does not output completions in a form which will be accepted by bash as single words.正如所写的那样,该完成功能不会以 bash 将其作为单个单词接受的形式输出完成。 In other words, the completion roger waters is generated as roger waters instead of roger\\ waters .换句话说,完成roger waters生成为roger waters而不是roger\\ waters In the (likely) case that the goal is to produce correctly quoted completions, it is necessary to add escapes a second time, after compgen filters the completion list:在目标是生成正确引用的补全(可能)的情况下,有必要在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
}

Note: I replaced the computation of $cur with $2 , since the function invoked through complete -F is passed the command as $1 and the word being completed as $2 .注意:我用$2替换了$cur的计算,因为通过complete -F调用的函数将命令作为$1传递,将要完成的单词作为$2传递。 (It's also passed the previous word as $3 .) Also, it's important to quote it, so that it doesn't get word-split on its way into compgen . (它也将前一个单词作为$3传递。)另外,引用它很重要,这样它就不会在进入compgen过程中被分compgen

If you need to process the data from the string you can use Bash's built-in string replacement operator.如果您需要处理字符串中的数据,您可以使用 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
}

Okay, this crazy contraption draws heavily on rici ’s solution, and not only fully works, but also quotes any completions that need it, and only those.好吧,这个疯狂的装置很大程度上借鉴了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

So as far as I could test it, it fully implements ls -like behavior, minus the path-specific parts.因此,就我可以测试而言,它完全实现了ls类的行为,减去了特定于路径的部分。

Verbose example详细示例

Here's a more verbose version of the _test function, so it becomes a bit more understandable:这是_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 }'
}

None of this is even remotely optimized for efficiency.这些甚至都没有针对效率进行远程优化。 Then again, this is only tab completion, and it's not causing a noticeable delay for any reasonably large list of completions.再说一次,这只是选项卡完成,对于任何相当大的完成列表,它不会造成明显的延迟。

It works by:它的工作原理是:

  1. Pulling the awk output into an array, using mapfile .使用mapfileawk输出拉入数组。
  2. Escaping the array and putting it into a string.转义数组并将其放入字符串中。
  3. Having a single space behind the %q as a separation marker.%q后面有一个空格作为分隔标记。
  4. Quoting $cur , Very important!引用$cur ,非常重要!
  5. Quoting the output of compgen .引用compgen输出 And only if it contains spaces.并且仅当它包含空格时。
  6. Feeding that output into COMPREPLY, using another mapfile call.使用另一个mapfile调用将该输出输入 COMPREPLY。
  7. Not using -o filenames .使用-o filenames

And it only works with all those tricks.它只适用于所有这些技巧。 It fails if even a single one is missing.即使缺少一个,它也会失败。 Trust me;相信我; I've tried.我试过了。 ;) ;)

I don't have mapfile available.我没有mapfile可用。 The following seems a bit simpler than the other answers and worked fine for me:以下似乎比其他答案更简单,对我来说效果很好:

_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}"
}
  • Each line in TOKEN_FILE is a completion option. TOKEN_FILE中的每一行都是一个完成选项。
  • Replace "${TOKEN_FILE}" with <(token_generator) where token_genorator is some command than generates completion tokens, on line at a time.<(token_generator)替换"${TOKEN_FILE}" ,其中token_genorator是一些命令而不是生成完成标记,一次在线。
  • The printf '%q' gives us a bash-escaped string suitable for consumption on the command line as a single token. printf '%q'为我们提供了一个 bash 转义字符串,适合在命令行上作为单个标记使用。

So, if TOKEN_FILE is:所以,如果TOKEN_FILE是:

option a
another option
an option with many spaces

Then tab-completing would print:然后制表符完成将打印:

an\ option\ with\ many\ spaces     another\ option
option\ a

The problem with OP's code is just the expansion of the output of compgen . OP 代码的问题只是compgen输出的扩展。 It uses shell's sloppy expansion COMPREPLY=(...) which breaks on every space.它使用 shell 的草率扩展COMPREPLY=(...) ,它在每个空间都中断。

Replace that line with将该行替换为

mapfile -t COMPREPLY < <( compgen -W "$use" -- $cur )

and the code works fine.并且代码工作正常。

Note that Shellcheck would have complained about that COMPREPLY=(...) expansion, and would have suggested using mapfile instead (see here ).请注意, Shellcheck会抱怨COMPREPLY=(...)扩展,并会建议改用mapfile (请参阅此处)。

Also, note that you don't see the escape character in the suggested completions, but when you press TAB the shell will add it to the command.另外请注意,您在建议的完成中看不到转义字符,但是当您按TAB键时,shell 会将其添加到命令中。 If you start your argument with quotes (either single or double), the shell will use quotes (the one you chose) instead of backslashes to protect the whitespaces.如果您以引号(单引号或双引号)开始您的论点,shell 将使用引号(您选择的引号)而不是反斜杠来保护空格。 As long as COMPREPLY is built properly, this all comes for free, so there's no need to mangle the completion patterns.只要正确构建COMPREPLY ,这一切都是免费的,因此无需破坏完成模式。


As others pointed out, instead of as an escaped string, the command providing the expansion patterns could return them one per line (like eg awk or ls ).正如其他人指出的那样,提供扩展模式的命令可以每行返回一个(例如awkls ),而不是作为转义字符串。 As you now surely know, you can read the output into an array using mapfile :正如您现在肯定知道的那样,您可以使用mapfile将输出读入数组:

mapfile -t my_array < <(the_command)

The problem is that compgen wants a string and not an array, so you need to expand it again, but you need to escape the whitespaces inside the single elements.问题是compgen想要一个字符串而不是一个数组,所以你需要再次展开它,但你需要转义单个元素内的空格。 This is probably the only tricky part of the whole process.这可能是整个过程中唯一棘手的部分。

Fortunately, you can do array expansion and substring substitution at the same time:幸运的是,您可以同时进行数组扩展和子字符串替换:

"${my_array[*]// /\\ }"

and you can feed the result directly to compgen :您可以将结果直接提供给compgen

function _test() {
    local use cur
    cur=${COMP_WORDS[COMP_CWORD]}
    mapfile -t use < <(pink)
    mapfile -t COMPREPLY < <(compgen -W "${use[*]// /\\ }" -- "$cur")
}

Disclaimer : I apologize for the many concepts here that are already present in the previous answers.免责声明:对于之前答案中已经存在的许多概念,我深表歉意。 I just wanted to stress that properly splitting the completions into the COMPREPLY array is the only requirement for tab completion.我只是想强调将补全正确地拆分到COMPREPLY数组中是 Tab 补全的唯一要求。 The shell takes care of everything after that, so it's not that difficult after all.之后 shell 会处理所有事情,所以它毕竟不是那么困难。 Moreover, as a teaching habit, I prefer to work out the problems in the OP's solution before providing a reference solution.而且,作为一种教学习惯,我更喜欢在提供参考方案之前解决OP方案中的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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