妥善处理在bash完成空间和报价(Properly handling spaces and quot

2019-06-26 06:37发布

什么是处理空间和报价在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' (尽管它与表达的文件名的某些方面的问题时,一个同时使用的(有效的)组合"'和各种逃脱),但我无法弄清楚如何ls通过阅读bash补码做的。

因此,没有任何人知道如何正确处理呢? 实际输入引号不必保存; 我将很高兴与一个解决方案,变化'words "b<tab>' 'words 'b<tab>''words b<tab>''words bar\ ' ,例如,(虽然我宁愿剥行情,就像在这个例子中,而不是将它们添加)。

Answer 1:

这不是太高雅了后处理解决方案,似乎为我工作(GNU的bash,版本3.1.17(6)-release(i686的-PC-cygwin的))。 (除非我没有测试一些边界情况下像往常一样:))

不需要EVAL的东西,只有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
}


Answer 2:

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

  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

TAB键的话

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

立即有两点我想澄清,以避免任何混淆:

  • 首先,你完成的功能不能简单地通过设置实现COMPREPLY到您的单词列表的数组! 上面的例子是硬编码返回与酒吧开始只是为了显示当按下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

TAB键的话

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来过滤的话。 (它是由庆典为此确切的目的提供。)人们可以使用他们喜欢的任何解决方案,但我建议不要使用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是零。



Answer 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 


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


Answer 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


文章来源: Properly handling spaces and quotes in bash completion