使用空格完成bash选项卡

时间:2014-10-22 13:59:20

标签: bash bash-completion

当可能的选项可能包含空格时,我遇到了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,以防万一...)

4 个答案:

答案 0 :(得分:5)

如果您需要处理字符串中的数据,可以使用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
}

答案 1 :(得分:5)

自定义标签 - 填写可能包含空格的单词非常困难。据我所知,没有优雅的解决方案。也许某些未来版本的compgen将足以产生一个数组而不是一次输出一行的可能性,甚至接受数组中的wordlist参数。但在此之前,以下方法可能有所帮助。

理解这个问题非常重要,因为( $(compgen ... ) )是通过将compgen命令的输出分割为$IFS中的字符而产生的数组, default是任何空格字符。因此,如果compgen返回:

roger waters
richard wright

然后COMPREPLY将有效地设置为数组(roger waters richard wright),总共有四个可能的完成次数。如果您改为使用( "$(compgen ...)"),则COMPREPLY将设置为数组($'roger waters\nrichard wright'),该数组只有一个可能的完成(在完成内部有换行符)。这些都不是你想要的。

如果所有可能的完成都没有换行符,那么您可以通过暂时重置compgen然后恢复它来安排在换行符处分割IFS返回。但我认为更优雅的解决方案是使用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。)

这并不能解决问题的另一个令人烦恼的部分,即将单词列表修改为compgen可接受的形式。由于compgen不允许多个-W选项,也不接受数组,因此唯一的选择是以bash分词(带引号)的方式格式化字符串和所有)将生成所需的列表。实际上,这意味着手动添加转义,就像您在函数pink中所做的那样:

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

但那容易发生意外和烦恼。更好的解决方案将允许直接指定替代品,特别是如果以某种方式生成替代品。生成可能包含空格的替代方法的一种好方法是将它们放入数组中。给定数组,您可以充分利用printf的{​​{1}}格式为%q生成正确引用的输入字符串:

compgen -W

如上所述,该完成函数不会以bash作为单个单词接受的形式输出完成。换句话说,完成# 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" ) } 生成为roger waters而不是roger waters。在(可能)情况下,目标是生成正确引用的完成,在roger\ waters过滤完成列表之后,有必要再次添加转义:

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 } 的计算替换为$cur,因为通过$2调用的函数将命令作为complete -F传递,并且单词将以{{{ 1}}。 (它也将前一个单词作为$1传递。)另外,引用它是很重要的,这样它就不会在进入{{1 }}

答案 2 :(得分:1)

好吧,这个疯狂的装置很大程度上依赖于 rici 的解决方案,不仅完全有效,而且还引用任何需要它的完成,而那些。< / p>

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. 使用awkmapfile输出拉入数组。
  2. 转义数组并将其放入字符串中。
  3. %q后面有一个空格作为分隔标记。
  4. 引用$cur,非常重要!
  5. 引用compgen输出。并且只有它包含空格。
  6. 使用其他 mapfile电话将该输出提供给COMPREPLY。
  7. 使用-o filenames
  8. 只有 才能使用所有这些技巧。如果缺少一个,它就会失败。相信我;我试过了。 ;)

答案 3 :(得分:0)

我没有可用的 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_FILE}" 替换 <(token_generator),其中 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