我想为逗号分隔的参数列表创建完成规则。例如,我有接收服务器名称列表的命令:
myscript -s name1,name2,name3
此时我已经成功地写出了以下完成内容:
_myscript () {
local cur prev opts
_get_comp_words_by_ref cur prev
opts='-s'
servers='name1 name2 name3'
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
else
case "${prev}" in
-s)
if [[ "$cur" == *,* ]]; then
local realcur prefix
realcur=${cur##*,}
prefix=${cur%,*}
COMPREPLY=( $(compgen -W "${servers}" -P "${prefix}," -- ${realcur}) )
else
COMPREPLY=( $(compgen -W "${servers}" -- ${cur}) )
fi
;;
*)
# do nothing
;;
esac
fi
}
但它至少有两个问题:
- 当前值的建议包括其前缀中的所有先前值。
- 它不考虑重复值。
对于此类情况,最佳做法是什么?也许 bash-completions 有一些 csv 列表的捆绑函数?
答案1
基本上没有办法解决您描述的问题,因为 bashCOMPREPLY
直接在显示中使用值,然后替换用户的文本 - 而为了获得您想要的内容,您需要首先生成可能的补全(只是额外的服务器名称,没有前缀)供 bash 显示,那么当 bash 即将用最长的非冲突字符串替换用户文本时,您需要它再次调用脚本以生成带有前缀 - 和 bash 的文本没有这方面的设施。
我能想到的最好办法是COMPREPLY
仅使用具有整个前缀(COMPREPLY=( "${prefix},"$(compgen -W "${servers[@]}" -- ${realcur}) )
)的第一个单词来生成,这样如果只有一个可能的完成,它会自动正确完成,而如果有多个可能的完成,那么 bash 不会删除到目前为止输入的内容(因为第一个单词COMPREPLY
具有完整的前缀,因此与当前输入的文本匹配,并且将被 bash 选择来替换用户的文本),并且将显示不带前缀的选项 - 除了对于已经包含前缀的单词,因此输出将如下所示:
$ command -s banana,a
ananas apricot banana,apple
“apple”在完成选项中排在最后,因为它带有以“b”开头的前缀 - 非常令人困惑。所以我不建议这样做。
关于重复项 - 为了不显示重复项,您只需要分解$prefix
其部分(简单IFS="," prefix_parts=($prefix)
:),然后迭代它们,只保留$servers
尚未列出的名称。打字很乏味,所以我不会在这里展示它,但相对琐碎,所以我相信你可以管理:-)。
总而言之,我认为您不应该对输入选项使用逗号分隔值,至少如果您希望 bash 帮助您完成的话。
您可以支持这样的选项格式:command -s <server> [<server> [..]]
然后,为了完成除紧接在该选项之后的条目以外的条目-s
,只需扫描数组,$COMP_WORDS
直到$COMP_CWORD
找到一个选项(与 匹配的字符串-*
),如果它是“-s”,则您需要完成服务器名称。
答案2
有一种方法可以解决列出的两个问题,但需要使用:
作为分隔符。这是因为冒号被 readline 或其他东西特殊对待。因此,您的示例将具有以下语法:
myscript -s name1:name2:name3
如果您的任何名字中包含冒号,则该方法将不起作用。
您的示例的完成脚本将是:
_myscript () {
local cur prev words cword
_init_completion -n : || return
local opts servers
opts='-s'
servers=(
name1
name2
name3
)
# also assume first argument will be an option
if [[ ${cur} == -* || $cword -eq 1 ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
else
case "${prev}" in
-s)
if [[ "$cur" == *:* ]]; then
local realcur prefix chosen remaining
realcur="${cur##*:}"
prefix="${cur%:*}"
chosen=()
IFS=$':\n' read -ra chosen <<< "$prefix"
remaining=()
readarray -t remaining <<< "$(printf '%s\n' "${servers[@]}" "${chosen[@]}" | sort | uniq -u)"
if [[ ${#remaining[@]} -gt 0 ]]; then
COMPREPLY=( $(compgen -W "${remaining[*]}" -- "$realcur") )
# add separator if user tabs again after entering a complete name
if [[ ${#COMPREPLY[@]} -eq 1 && ${#remaining[@]} -gt 0 && "$realcur" == "${COMPREPLY[0]}" ]]; then
COMPREPLY=("${COMPREPLY[0]}:")
fi
if [[ ${#remaining[@]} -gt 1 ]]; then
compopt -o nospace
fi
fi
else
COMPREPLY=( $(compgen -W "${servers[*]}" -- "$cur") )
# add separator if user tabs again after entering a complete name
if [[ ${#COMPREPLY[@]} -eq 1 && "$cur" == "${COMPREPLY[0]}" ]]; then
COMPREPLY=("${COMPREPLY[0]}:")
fi
compopt -o nospace
fi
;;
*)
# do nothing
;;
esac
fi
} &&
complete -F _myscript myscript