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

Kar*_*mer 26 bash quotes eval autocomplete escaping

在bash完成中处理空格和引号的正确/最佳方法是什么?

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

foo
bar one
bar two
Run Code Online (Sandbox Code Playgroud)

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

_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
Run Code Online (Sandbox Code Playgroud)

‘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 || "")
Run Code Online (Sandbox Code Playgroud)

这实际上有效.无论‘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’(尽管它有表达的文件名某些方面的问题时,一个同时使用的(有效)的组合",'以及各种逃逸).但是,我无法ls通过阅读bash完成代码来弄清楚它是如何做到的.

那么,有人知道如何妥善处理这个问题吗?不需要保留实际的输入引号; 我将很高兴与变化的解决方案‘words "b<tab>’,‘words 'b<tab>’‘words b<tab>’‘words bar\ ’,例如,(虽然我在这个例子中喜欢剥报价一样,而不是增加他们的).

ant*_*tak 18

问题是相当加载的,这个答案试图解释每个方面:

  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 ?
Run Code Online (Sandbox Code Playgroud)

Tab

$ words bar\ ?          # Ex.1: notice the space is completed escaped
Run Code Online (Sandbox Code Playgroud)

TabTab

bar one  bar two        # Ex.2: notice the spaces are displayed unescaped
$ words bar\ ?
Run Code Online (Sandbox Code Playgroud)

现在,我应该立即明确两点,以避免任何混淆:

  • 首先,只需设置COMPREPLY单词列表数组就无法实现完成功能!上面的示例是硬编码的,用于返回以bar开头的候选项,只是为了显示TabTab按下时会发生什么.(别担心,我们很快就会进行更全面的实施!)

  • 其次,该格式COMPREPLY只能是因为 -o filenames被指定.有关如何设置COMPREPLY何时不使用的说明-o filenames,请查看下一个子标题.

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

如何处理空间COMPREPLY(不使用-o filenames)

长话短说,它需要被逃脱,这就是@Eugene所接受的答案.

为了对比上述-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 ?
Run Code Online (Sandbox Code Playgroud)

Tab

$ words bar\ ?          # Same as -o filenames, space is completed escaped
Run Code Online (Sandbox Code Playgroud)

TabTab

bar\ one  bar\ two      # Unlike -o filenames, notice the spaces are displayed escaped
$ words bar\ ?
Run Code Online (Sandbox Code Playgroud)

我如何实际实现完成功能?

实现完成功能包括:

  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
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我们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))`
Run Code Online (Sandbox Code Playgroud)

最后,我们设置COMPREPLY确保逃避空间的喜欢.转义是非常复杂的,但幸运printf的是,%q格式执行了我们需要的所有必要的转义,这就是我们用来扩展的东西CANDIDATES.(注意我们告诉我们在每个元素之后printf放置\n,因为这是我们设置IFS的.)

那些观察者可能会发现这种形式的设置COMPREPLY仅适用于-o filenames 未使用的情况.如果是,COMPREPLY则不需要转义,并且可以设置为CANDIDATES与之相同的内容COMPREPLY=("$CANDIDATES[@]").

在可能对空阵列执行扩展时应格外小心,因为这会导致意外结果.上面的例子通过在长度CANDIDATES为零时分支来处理这个问题.

  • 如果你这样说 作为答案,我冒昧地提供了一个充分运作的例子. (2认同)

Eug*_*ene 8

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

不需要评估东西,只有2种报价.

因为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
}
Run Code Online (Sandbox Code Playgroud)


Orw*_*ile 5

_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 
Run Code Online (Sandbox Code Playgroud)

  • @ttsiodras谢谢,那是因为我只是在尝试解决自己的问题时才来,所以当我对找到的答案不满意时,我会自己解决,然后再提供一个“体面的答案” * 。它并没有为我赢得很多分,但是像您这样的评论确实值得。 (2认同)