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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1146098/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-17 21:04:12  来源:igfitidea点击:

Properly handling spaces and quotes in bash completion

bashautocompleteescapingevalquotes

提问by Karl Ove Hufthammer

What is the correct/best way of handling spaces and quotes in bash completion?

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

Here's a simple example. I have a command called words(e.g., a dictionary lookup program) that takes various words as arguments. The supported ‘words' may actually contain spaces, and are defined in a file called words.dat:

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

foo
bar one
bar two

Here's my first suggested solution:

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

_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

Typing ‘words f<tab>'correctly completes the command to ‘words foo '(with a trailing space), which is nice, but for ‘words b<tab>'it suggests ‘words bar '. The correct completion would be ‘words bar\ '. And for ‘words "b<tab>'and ‘words 'b<tab>'it offers no suggestions.

‘words f<tab>'正确键入会完成命令 to ‘words foo '(带有尾随空格),这很好,但因为‘words b<tab>'它建议‘words bar '. 正确的完成是‘words bar\ '. 而对于‘words "b<tab>'‘words 'b<tab>'它不提供建议。

This last part I have been able to solve. It's possible to use evalto properly parse the (escaped) characters. However, evalis not fond of missing quotes, so to get everything to work, I had to change the search="$cur"to

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

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

This actually works. Both ‘words "b<tab>'and ‘words 'b<tab>'correctly autocompletes, and if I add a ‘o'and press <tab>again, it actually completes the word and adds the correct closing quote. However, if I try to complete ‘words b<tab>'or even ‘words bar\ <tab>', it is autocompleted to ‘words bar 'instead of ‘words bar\ ', and adding for instance ‘one'would fail when the wordsprogram is run.

这实际上有效。双方‘words "b<tab>'‘words 'b<tab>'正确地自动完成的,如果我添加了一个‘o',然后按<tab>一次,它实际上完成了字,并添加正确的收盘报价。但是,如果我尝试完成‘words b<tab>'甚至‘words bar\ <tab>',它会自动完成‘words bar '而不是‘words bar\ ',并且‘one'words程序运行时添加例如会失败。

Now, obviously it ispossible to handle this correctly. For instance, the lscommand can do it for files namned ‘foo'‘bar one'and ‘bar two'(though it does have problems with some ways of expressing the filenames when one uses a (valid) combination of both ", 'and various escapes). However, I couldn't figure out how lsdoes it by reading the bash completion code.

现在,很明显它能够正确地处理这个问题。例如,该ls命令可以namned文件做到这一点‘foo'‘bar one',并‘bar two'(尽管它有表达的文件名某些方面的问题时,一个同时使用的(有效)的组合"'以及各种逃逸)。但是,我无法ls通过阅读 bash 完成代码来弄清楚它是如何做到的。

So, does anybody know of how properly handle this? The actual input quotes need not be preserved; I would be happy with a solution that changes ‘words "b<tab>', ‘words 'b<tab>'and ‘words b<tab>'to ‘words bar\ ', for instance, (though I would prefer stripping of quotes, like in this example, instead of adding them).

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

采纳答案by Eugene

This not too elegant postprocessing solution seems to work for me (GNU bash, version 3.1.17(6)-release (i686-pc-cygwin)). (Unless I didn't test some border case as usual :))

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

Don't need to eval things, there are only 2 kinds of quotes.

不需要评估的东西,只有两种引号。

Since compgen doesn't want to escape spaces for us, we will escape them ourselves (only if word didn't start with a quote). This has a side effect of full list (on double tab) having escaped values as well. Not sure if that's good or not, since ls doesn't do it...

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

EDIT: Fixed to handle single and double qoutes inside the words. Essentially we have to pass 3 unescapings :). First for grep, second for compgen, and last for words command itself when autocompletion is done.

编辑:已修复以处理单词内的单引号和双引号。本质上,我们必须通过 3 个转义:)。第一个用于 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
}

回答by antak

The question is rather loaded but this answer attempts to explain each aspect:

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

  1. How to handle spaces with COMPREPLY.
  2. How does lsdo it.
  1. 如何处理空格COMPREPLY
  2. 是怎么ls做的。

There're also people reaching this question wanting to know how to implement the completion function in general. So:

也有人提出这个问题想知道如何实现完成功能。所以:

  1. How how do I implement the completion function and correctly set COMPREPLY?
  1. 如何实现完成功能并正确设置COMPREPLY

How does lsdo it

怎么ls

Moreover, why does it behave differently to when I set COMPREPLY?

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

Back in '12 (before I updated this answer), I was in a similar situation and searched high and low for the answer to this discrepancy myself. Here's the answer I came up with.

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

ls, or rather, the default completion routine does it using the -o filenamesfunctionality. This option performs: filename-specific processing (like adding a slash to directory names or suppressing trailing spaces.

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

To demonstrate:

展示:

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

Tab

Tab

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

TabTab

TabTab

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

Immediately there are two points I want to make clear to avoid any confusion:

为了避免任何混淆,我想立即说明两点:

  • First of all, your completion function cannot be implemented simply by setting COMPREPLYto an array of your word list! The example above is hard-codedto return candidates starting with b-a-r just to show what happens when TabTabis pressed. (Don't worry, we'll get to a more general implementation shortly.)

  • Second, the above format for COMPREPLYonly works because -o filenamesis specified. For an explanation of how to set COMPREPLYwhen not using -o filenames, look no further than the next heading.

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

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

Also note, there's a downside of using -o filenames: If there's a directory lying about with the same name as the matching word, the completed word automatically gets an arbitrary slash attached to the end. (e.g. bar\ one/)

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

How to handle spaces with COMPREPLYwithout using -o filenames

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

Long story short, it needs to be escaped.

长话短说,它需要逃脱。

In contrast to the above -o filenamesdemo:

与上面的-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 ?

Tab

Tab

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

TabTab

TabTab

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

How do I actually implement a completion function?

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

Implementing a completion functions involves:

实现一个完成功能包括:

  1. Representing your word list.
  2. Filtering your word list to just candidates for the current word.
  3. Setting COMPREPLYcorrectly.
  1. 代表您的单词列表。
  2. 将您的单词列表过滤为当前单词的候选词。
  3. COMPREPLY正确设置。

I'm not going to assume to know all the complex requirements there can be for 1 and 2 and the following is only a very basic implementation. I'm providing an explanation for each part so one can mix-and-match to fit their own requirements.

我不会假设知道 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

In this example, we use compgento filter our words. (It's provided by bash for this exact purpose.) One could use any solution they like but I'd advise against using grep-like programs simply because of the complexities of escaping regex.

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

compgentakes the word list with the -Wargument and returns the filtered result with one word per line. Since our words can contain spaces, we set IFS=$'\n'beforehand in order to only count newlines as element delimiters when putting the result into our array with the CANDIDATES=(...)syntax.

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

Another point of note is what we're passing for the -Wargument. This argument takes an IFSdelimited word list. Again, our words contain spaces so this too requires IFS=$'\n'to prevent our words being broken up. Incidentally, "${WORD_LIST[*]}"expands with elements also delimited with what we've set for IFSand is exactly what we need.

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

In the example above I chose to define WORD_LISTliterally in code.

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

One could also initialize the array from an external source such as a file. Just make sure to move IFS=$'\n'beforehand if words are going to be line-delimited such as in the original question:

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

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

Finally, we set COMPREPLYmaking sure to escape the likes of spaces. Escaping is quite complicated but thankfully printf's %qformat performs all the necessary escaping we need and that's what we use to expand CANDIDATES. (Note we're telling printfto put \nafter each element because that's what we've set IFSto.)

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

Those observant may spot this form for COMPREPLYonly applies if -o filenamesis not used. No escaping is necessary if it is and COMPREPLYmay be set to the same contents as CANDIDATESwith COMPREPLY=("$CANDIDATES[@]").

细心的人可能会发现此表格COMPREPLY仅适用于-o filenames未使用的情况。无法回避是必要的,如果它是和COMPREPLY可以被设置为相同的内容CANDIDATESCOMPREPLY=("$CANDIDATES[@]")

Extra care should be taken when expansions may be performed on empty arrays as this can lead to unexpected results. The example above handles this by branching when the length of CANDIDATESis zero.

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

回答by Orwellophile

_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 

回答by n.r.

Pipe _find_wordsthrough sedand have it enclose each line in quotation marks. And when typing a command line, make sure to put either "or 'before a word to be tab-completed, otherwise this method will not work.

_find_words通过管道sed并将其括在引号中。并且在输入命令行的时候,一定要在需要tab补全的单词之前加上其中之一"或者'之前,否则这个方法是行不通的。

_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

Command line:

命令行:

$ words "ba?

tab

tab

$ words "bar ?

tabtab

tabtab

bar one  bar two
$ words "bar o?

tab

tab

$ words "bar one" ?

回答by Jake

I solved this by creating my own function compgen2 which handles the extra processing when the current word doesn't begin with a quote character. otherwise it works similar to compgen -W.

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

compgen2() {
    local IFS=$'\n'
    local a=($(compgen -W "" -- ""))
    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