Bash 完成逗号分隔值

Bash 完成逗号分隔值

我想为逗号分隔的参数列表创建完成规则。例如,我有接收服务器名称列表的命令:

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
}

但它至少有两个问题:

  1. 当前值的建议包括其前缀中的所有先前值。
  2. 它不考虑重复值。

对于此类情况,最佳做法是什么?也许 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

相关内容