在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\ ’
的解决方案感到满意(虽然我更喜欢剥离引号,例如,在此示例中,而不是添加它们。)
答案 0 :(得分:17)
这个问题很重要,但这个答案试图解释每个方面:
COMPREPLY
处理空格。ls
如何做到这一点。也有人想要知道如何实施这个问题 完成功能一般。所以:
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开头的候选项,以显示按下 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\ ░
实施完成功能包括:
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
的{{1}}格式执行了我们需要的所有必要转义,这就是我们用来展开%q
的内容。 (请注意,我们告诉CANDIDATES
将printf
放在每个元素之后,因为这是我们设置\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_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" ░
答案 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