【问题标题】:Properly handling spaces and quotes in bash completion正确处理 bash 完成中的空格和引号
【发布时间】:2019-03-04 12:06:33
【问题描述】:

在 bash 补全中处理空格和引号的正确/最佳方式是什么?

这是一个简单的例子。我有一个名为 words 的命令(例如,一个字典查找程序),它将各种单词作为参数。支持的“单词”实际上可能包含空格,并在名为 words.dat 的文件中定义:

foo
bar one
bar two

这是我的第一个建议解决方案:

_find_words()
{
search="$cur"
grep -- "^$search" words.dat
}

_words_complete()
{
local IFS=$'\n'

COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"

COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

}
complete -F _words_complete words

输入‘words f<tab>’ 正确完成了对‘words foo ’ 的命令(带有尾随空格),这很好,但对于‘words b<tab>’,它建议‘words bar ’。正确的完成是‘words bar\ ’。对于‘words "b<tab>’‘words 'b<tab>’,它不提供任何建议。

我已经能够解决最后一部分。可以使用eval 正确解析(转义)字符。但是,eval 不喜欢缺少引号,所以为了让一切正常工作,我不得不将 search="$cur" 更改为

search=$(eval echo "$cur" 2>/dev/null ||
eval echo "$cur'" 2>/dev/null ||
eval echo "$cur\"" 2>/dev/null || "")

这确实有效。 ‘words "b<tab>’‘words 'b<tab>’ 都正确地自动完成,如果我添加 ‘o’ 并再次按 <tab>,它实际上会完成单词并添加正确的结束引号。但是,如果我尝试完成 ‘words b<tab>’ 甚至 ‘words bar\ <tab>’,它会自动完成为 ‘words bar ’ 而不是 ‘words bar\ ’,并且在运行 words 程序时添加 ‘one’ 会失败。

现在,显然 可以正确处理这个问题。例如,ls 命令可以对名为‘foo’‘bar one’‘bar two’ 的文件执行此操作(尽管当使用" 两者的(有效)组合时,它确实存在一些表达文件名的方法的问题,' 和各种转义)。但是,通过阅读 bash 完成代码,我无法弄清楚 ls 是如何做到的。

那么,有人知道如何正确处理这个问题吗?不需要保留实际的输入报价;例如,我会对将‘words "b<tab>’‘words 'b<tab>’‘words b<tab>’ 更改为‘words bar\ ’ 的解决方案感到满意(尽管我更喜欢像本例中那样去掉引号,而不是添加它们)。

【问题讨论】:

    标签: bash autocomplete escaping eval quotes


    【解决方案1】:

    这个问题相当丰富,但这个答案试图解释每个方面:

    1. 如何使用COMPREPLY 处理空格。
    2. ls是怎么做到的。

    也有人提出这个问题,想知道如何实施 一般的完成功能。所以:

    1. 如何实现补全功能并正确设置COMPREPLY

    ls是怎么做到的

    此外,为什么它的行为与我设置 COMPREPLY 时的行为不同?

    早在 12 年(在我更新此答案之前),我就处于类似的情况,我自己在高低搜索了这个差异的答案。这是我想出的答案。

    ls,或者更确切地说,默认完成例程使用-o filenames 功能来完成。此选项执行:特定于文件名的处理(例如在目录名称中添加斜杠或禁止尾随空格

    演示:

    $ foo () { COMPREPLY=("bar one" "bar two"); }
    $ complete -o filenames -F foo words
    $ words ░
    

    制表符

    $ words bar\ ░          # Ex.1: notice the space is completed escaped
    

    制表符制表符

    bar one  bar two        # Ex.2: notice the spaces are displayed unescaped
    $ words bar\ ░
    

    我想立即说明两点以避免混淆:

    • 首先,您的补全功能不能简单地通过将COMPREPLY 设置为您的单词列表数组来实现!上面的示例硬编码返回以 b-a-r 开头的候选者,只是为了显示按下 TabTab 时会发生什么。 (别担心,我们很快就会进行更通用的实现。)

    • 其次,COMPREPLY 的上述格式仅适用于指定了-o filenames。有关在不使用 -o filenames 时如何设置 COMPREPLY 的说明,请看下一个标题。

    另外请注意,使用-o filenames 有一个缺点:如果存在与匹配单词同名的目录,则完成的单词会自动在末尾附加任意斜线。 (例如bar\ one/

    如何在不使用-o filenames 的情况下使用COMPREPLY 处理空格

    长话短说,需要转义。

    对比上面的-o filenames演示:

    $ foo () { COMPREPLY=("bar\ one" "bar\ two"); }     # Notice the blackslashes I've added
    $ complete -F foo words                             # Notice the lack of -o filenames
    $ words ░
    

    制表符

    $ words bar\ ░          # Same as -o filenames, space is completed escaped
    

    制表符制表符

    bar\ one  bar\ two      # Unlike -o filenames, notice the spaces are displayed escaped
    $ words bar\ ░
    

    如何实际实现完成功能?

    实现一个完成函数包括:

    1. 代表您的单词列表。
    2. 将您的单词列表过滤为当前单词的候选词。
    3. 正确设置COMPREPLY

    我不会假设知道 1 和 2 可能存在的所有复杂要求,以下只是一个非常基本的实现。我正在为每个部分提供解释,以便人们可以混合搭配以满足自己的要求。

    foo() {
        # Get the currently completing word
        local CWORD=${COMP_WORDS[COMP_CWORD]}
    
        # This is our word list (in a bash array for convenience)
        local WORD_LIST=(foo 'bar one' 'bar two')
    
        # Commands below depend on this IFS
        local IFS=$'\n'
    
        # Filter our candidates
        CANDIDATES=($(compgen -W "${WORD_LIST[*]}" -- "$CWORD"))
    
        # Correctly set our candidates to COMPREPLY
        if [ ${#CANDIDATES[*]} -eq 0 ]; then
            COMPREPLY=()
        else
            COMPREPLY=($(printf '%q\n' "${CANDIDATES[@]}"))
        fi
    }
    
    complete -F foo words
    

    在这个例子中,我们使用compgen 来过滤我们的单词。 (它由 bash 提供用于这个确切目的。)人们可以使用他们喜欢的任何解决方案,但我建议不要使用类似 grep 的程序,因为转义正则表达式的复杂性。

    compgen 获取带有-W 参数的单词列表,并返回过滤后的结果,每行一个单词。由于我们的单词可以包含空格,因此我们预先设置了IFS=$'\n',以便在使用CANDIDATES=(...) 语法将结果放入我们的数组时仅将换行符计为元素分隔符。

    另一个值得注意的地方是我们为-W 参数传递的内容。此参数采用IFS 分隔的单词列表。同样,我们的单词包含空格,所以这也需要IFS=$'\n' 以防止我们的单词被打断。 顺便说一句,"${WORD_LIST[*]}" 扩展的元素也用我们为IFS 设置的内容分隔,这正是我们所需要的。

    在上面的示例中,我选择在代码中逐字定义WORD_LIST

    也可以从外部源(例如文件)初始化数组。如果单词要以行分隔,例如在原始问题中,请确保事先移动IFS=$'\n'

    local IFS=$'\n'
    local WORD_LIST=($(cat /path/to/words.dat))`
    

    最后,我们设置COMPREPLY 以确保避开空格之类的内容。转义非常复杂,但幸运的是printf%q 格式执行了我们需要的所有必要转义,这就是我们用来扩展CANDIDATES 的方法。 (请注意,我们告诉printf 在每个元素后面加上\n,因为这是我们设置的IFS。)

    那些细心的人可能会发现COMPREPLY 的此表单仅适用于-o filenames 未使用。如果是,则无需转义,并且COMPREPLY 可以设置为与CANDIDATESCOMPREPLY=("$CANDIDATES[@]") 相同的内容。

    当可能对空数组执行扩展时,应格外小心,因为这可能会导致意外结果。上面的示例通过在CANDIDATES 的长度为零时进行分支来处理此问题。

    【讨论】:

    • 您的第一个示例实际上不起作用。 TAB TAB 可以为您提供列表,但是当您尝试“words bar t”时它会失败。
    • 您误解了该示例的目的。如果您想实际实现一个 bash 完成例程,它需要的不仅仅是将 COMPREPLY 设置为静态数组。我确信在 SO 上有更合适的问题专门处理这个问题。
    • 如果你这么说。我冒昧地提供了您的示例的完整工作示例,作为答案。
    【解决方案2】:

    这个不太优雅的后处理解决方案似乎对我有用(GNU bash,版本 3.1.17(6)-release (i686-pc-cygwin))。 (除非我没有像往常一样测试一些边界情况:))

    东西不用eval,引号只有2种。

    由于 compgen 不想为我们转义空格,我们将自己转义它们(仅当单词不以引号开头时)。这具有完整列表(在双标签上)也具有转义值的副作用。不确定这是否好,因为 ls 不这样做......

    编辑:固定处理单词中的单引号和双引号。基本上我们必须通过 3 次转义:)。第一个用于 grep,第二个用于 compgen,最后一个用于自动完成后的 words 命令本身。

    _find_words()
    {
        search=$(eval echo "$cur" 2>/dev/null || eval echo "$cur'" 2>/dev/null || eval echo "$cur\"" 2>/dev/null || "")
        grep -- "^$search" words.dat | sed -e "{" -e 's#\\#\\\\#g' -e "s#'#\\\'#g" -e 's#"#\\\"#g' -e "}"
    }
    
    _words_complete()
    {
        local IFS=$'\n'
    
        COMPREPLY=()
        local cur="${COMP_WORDS[COMP_CWORD]}"
    
        COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )
    
        local escaped_single_qoute="'\''"
        local i=0
        for entry in ${COMPREPLY[*]}
        do
            if [[ "${cur:0:1}" == "'" ]] 
            then
                # started with single quote, escaping only other single quotes
                # [']bla'bla"bla\bla bla --> [']bla'\''bla"bla\bla bla
                COMPREPLY[$i]="${entry//\'/${escaped_single_qoute}}" 
            elif [[ "${cur:0:1}" == "\"" ]] 
            then
                # started with double quote, escaping all double quotes and all backslashes
                # ["]bla'bla"bla\bla bla --> ["]bla'bla\"bla\\bla bla
                entry="${entry//\\/\\\\}" 
                COMPREPLY[$i]="${entry//\"/\\\"}" 
            else 
                # no quotes in front, escaping _everything_
                # [ ]bla'bla"bla\bla bla --> [ ]bla\'bla\"bla\\bla\ bla
                entry="${entry//\\/\\\\}" 
                entry="${entry//\'/\'}" 
                entry="${entry//\"/\\\"}" 
                COMPREPLY[$i]="${entry// /\\ }"
            fi
            (( i++ ))
        done
    }
    

    【讨论】:

    • 谢谢。此解决方案适用于原始示例,但如果我将“摇滚”添加到 words.dat,它会失败。我在现实生活中对自动补全的使用实际上涉及带有撇号的单词,这就是我最初使用 eval 的原因。通过在 `for 循环中添加一个额外的“搜索和替换”,然后为以 ' 开头的字符串添加另一个 for 循环,修复起来很容易(虽然不是很优雅)。据我所知,唯一剩下的问题是,如果你写了一个完整的单词,包括任何右引号,自动完成不会前进一个光标位置。
    • 关于我上面的评论。看起来情况比我想象的要糟糕一些。在包含撇号的单词中自动完成(例如,尝试自动完成“rock 'n ro”,使用转义空格和撇号,或单引号或双引号)不起作用。原因是search 变量不是正确的扩展形式。一些额外的替换似乎是可能的,但我无法让这对于所有三种不同的转义方式都能正常工作。
    • 是的,compgen 似乎正在剥离所有 qoutes... 这意味着它们必须在 _find_words 中转义
    • 如果您需要处理一些其他特殊字符($、`、{、(或任何其他 bash 可能会冒犯的东西——没有测试这些),只需以类似的方式转义它们——第一个反斜杠,然后是 2 或 3 个位置的其他所有内容。
    • 有些东西还是不行的,e.g.完成"rock 'n。其中大部分可以通过从_find_words 调用中删除"$cur" 来修复。然后一切都适用于以双引号开头或没有任何内容的字符串,除非“完成”以双引号开头的已经完整的单词(因为最后一个双引号错误地得到引号?)。并且以单引号开头的单词补全仍然存在一些问题(插入补全建议时似乎只能识别单词的最后一部分)。但总而言之,这个解决方案工作正常。谢谢!
    【解决方案3】:
    _foo ()
    {
      words="bar one"$'\n'"bar two"
      COMPREPLY=()
      cur=${COMP_WORDS[COMP_CWORD]}
      prev=${COMP_WORDS[COMP_CWORD-1]}
      cur=${cur//\./\\\.}
    
      local IFS=$'\n'
      COMPREPLY=( $( grep -i "^$cur" <( echo "$words" ) | sed -e 's/ /\\ /g' ) )
      return 0
    }
    
    complete -o bashdefault -o default -o nospace -F _foo words 
    

    【讨论】:

    • 你为什么要在$cur 上将. 转义为\.
    • 但是 bash 替换从来都不是正则表达式,对吗? 考虑一下:text='foo.bar'; echo "${text//./_}" - 在 GNU bash 4.3.11 (Linux) 和3.1.20 (Windows)。
    • 但是当它传递给 grep 时,grep 会将其解释为正则表达式。
    • 我的意思是,cur=${cur//./\\.} 有同样的效果。或者应该,这就是我问的原因。 (顺便说一句,在这种情况下,使用grep 的另一种方法是逐行分隔,将每一行修剪为模式字符串的长度,并测试两者是否相同——这就是我正在使用的,因为它不仅仅使用内置函数时速度更快,但避免使用对 grep 具有特殊含义的其他字符。
    • @ttsiodras 谢谢,那是因为我只有在尝试解决自己的问题时才来 SO,当我对找到的答案不满意时,我会自己解决,并且返回提供一个体面的答案。它并没有为我赢得很多积分,但像你这样的 cmets 确实值得。
    【解决方案4】:

    _find_words 连接到sed 并用引号将每一行括起来。并且在键入命令行时,请确保将"' 放在要使用制表符完成的单词之前,否则此方法将不起作用。

    _find_words() { cat words.dat; }
    
    _words_complete()
    {
    
      COMPREPLY=()
      cur="${COMP_WORDS[COMP_CWORD]}"
    
      local IFS=$'\n'
      COMPREPLY=( $( compgen -W "$( _find_words | sed 's/^/\x27/; s/$/\x27/' )" \
                             -- "$cur" ) )
    
    }
    
    complete -F _words_complete words
    

    命令行:

    $ words "ba░
    

    标签

    $ words "bar ░
    

    标签标签

    bar one  bar two
    $ words "bar o░
    

    标签

    $ words "bar one" ░
    

    【讨论】:

      【解决方案5】:

      我通过创建自己的函数 compgen2 解决了这个问题,该函数在当前单词不以引号字符开头时处理额外的处理。否则它的工作方式类似于 compgen -W。

      compgen2() {
          local IFS=$'\n'
          local a=($(compgen -W "$1" -- "$2"))
          local i=""
          if [ "${2:0:1}" = "\"" -o "${2:0:1}" = "'" ]; then
              for i in "${a[@]}"; do
                  echo "$i"
              done
          else
              for i in "${a[@]}"; do
                  printf "%q\n" "$i"
              done
          fi
      }
      
      _foo() {
          local cur=${COMP_WORDS[COMP_CWORD]}
          local prev=${COMP_WORDS[COMP_CWORD-1]}
          local words=$(cat words.dat)
          local IFS=$'\n'
          COMPREPLY=($(compgen2 "$words" "$cur"))
      }
      
      echo -en "foo\nbar one\nbar two\n" > words.dat
      complete -F _foo foo
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-07-21
        • 2021-12-18
        • 2012-11-15
        • 1970-01-01
        • 2013-10-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多