正确处理bash完成中的空格和引号

时间:2009-07-17 23:16:00

标签: bash autocomplete escaping eval quotes

在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\ ’,而实例‘one’的添加会在words程序正在运行。

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

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

5 个答案:

答案 0 :(得分:17)

这个问题很重要,但这个答案试图解释每个方面:

  1. 如何使用COMPREPLY处理空格。
  2. ls 如何做到这一点。
  3. 也有人想要知道如何实施这个问题 完成功能一般。所以:

    1. 如何实施完成功能并正确设置COMPREPLY
    2. 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开头的候选项,以显示按下 Tab Tab 时发生的情况。 (别担心,我们很快就会进行更全面的实施。)

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

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

      如何使用COMPREPLY处理空格而不使用-o filenames

      长话短说,它需要逃脱。

      与上述-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
      4. 我不会假设知道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的{​​{1}}格式执行了我们需要的所有必要转义,这就是我们用来展开%q的内容。 (请注意,我们告诉CANDIDATESprintf放在每个元素之后,因为这是我们设置\n的内容。)

        只有在IFS 未使用的情况下,COMPREPLY才会显示此表单。如果可以,则不需要转义,并且-o filenames可以设置为与COMPREPLY CANDIDATES相同的内容。

        在可能对空阵列执行扩展时应格外小心,因为这可能会导致意外结果。上面的示例通过在COMPREPLY=("$CANDIDATES[@]")的长度为零时进行分支来处理此问题。

答案 1 :(得分:8)

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

不需要评估东西,只有两种报价。

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

编辑:修复了处理单词内部的单个和双重qoutes。基本上我们必须通过3个unescapings :)。首先是grep,第二个是compgen,最后是单词命令自动完成自动完成。

_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
}

答案 2 :(得分:4)

_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 

答案 3 :(得分:1)

管道_find_wordssed并将每行用引号括起来。在键入命令行时,请确保在要完成制表符的单词之前放置"',否则此方法将无效。

_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" ░

答案 4 :(得分:0)

我通过创建自己的函数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