buf: Zsh/Fish completion failing

Hi there,

zsh completion is not working, I’m getting _arguments:comparguments:325: invalid option definition: --log-format[The log format [text,color,json].]:

Generated completion looks like:

function _buf {
  local -a commands

  _arguments -C \
    '--log-format[The log format [text,color,json].]:' \
    '--log-level[The log level [debug,info,warn,error].]:' \
    '--timeout[The duration until timing out.]:' \
    "1: :->cmnds" \
    "*::arg:->args"

It seems cobra doesn’t escape flag description properly: https://github.com/spf13/cobra/blob/v1.0.0/zsh_completions.go#L334 and braces from flag descriptions with permitted values break it.

I’m not sure whether it’s done on purpose for some flexibility or not.

% zsh --version
zsh 5.8 (x86_64-pc-linux-gnu)

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 18 (11 by maintainers)

Most upvoted comments

This should be fixed - please let us know if this comes up again! We won’t be surprised 😃

Hey @marckhouzam, thanks for investigating this and all of the information! We’ve actually already patched this internally and it will go out with our next release We should have updated this issue when we landed the change internally, sorry about that.

The issue with both zsh and fish completion here is going to be within the github.com/spf13/cobra library - they’ve been doing a lot of refactors to shell completion recently https://github.com/spf13/cobra/commits/master and there’s clearly some bugs. Unfortunately, we don’t have the time to dive into these right now, but this is where the bug would be. Will keep this open, and will re-test as improvements in spf13/cobra are available.

@bufdev the error is fixed, but completion is gone now btw 😃 Didn’t have much time to figure out why, but generated code does nothing now, it doesn’t contain nether command names nor flags.

#compdef _buf buf

# zsh completion for buf                                  -*- shell-script -*-

__buf_debug()
{
    local file="$BASH_COMP_DEBUG_FILE"
    if [[ -n ${file} ]]; then
        echo "$*" >> "${file}"
    fi
}

_buf()
{
    local shellCompDirectiveError=1
    local shellCompDirectiveNoSpace=2
    local shellCompDirectiveNoFileComp=4
    local shellCompDirectiveFilterFileExt=8
    local shellCompDirectiveFilterDirs=16

    local lastParam lastChar flagPrefix requestComp out directive compCount comp lastComp
    local -a completions

    __buf_debug "\n========= starting completion logic =========="
    __buf_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}"

    # The user could have moved the cursor backwards on the command-line.
    # We need to trigger completion from the $CURRENT location, so we need
    # to truncate the command-line ($words) up to the $CURRENT location.
    # (We cannot use $CURSOR as its value does not work when a command is an alias.)
    words=("${=words[1,CURRENT]}")
    __buf_debug "Truncated words[*]: ${words[*]},"

    lastParam=${words[-1]}
    lastChar=${lastParam[-1]}
    __buf_debug "lastParam: ${lastParam}, lastChar: ${lastChar}"

    # For zsh, when completing a flag with an = (e.g., buf -n=<TAB>)
    # completions must be prefixed with the flag
    setopt local_options BASH_REMATCH
    if [[ "${lastParam}" =~ '-.*=' ]]; then
        # We are dealing with a flag with an =
        flagPrefix="-P ${BASH_REMATCH}"
    fi

    # Prepare the command to obtain completions
    requestComp="${words[1]} __complete ${words[2,-1]}"
    if [ "${lastChar}" = "" ]; then
        # If the last parameter is complete (there is a space following it)
        # We add an extra empty parameter so we can indicate this to the go completion code.
        __buf_debug "Adding extra empty parameter"
        requestComp="${requestComp} \"\""
    fi

    __buf_debug "About to call: eval ${requestComp}"

    # Use eval to handle any environment variables and such
    out=$(eval ${requestComp} 2>/dev/null)
    __buf_debug "completion output: ${out}"

    # Extract the directive integer following a : from the last line
    local lastLine
    while IFS='\n' read -r line; do
        lastLine=${line}
    done < <(printf "%s\n" "${out[@]}")
    __buf_debug "last line: ${lastLine}"

    if [ "${lastLine[1]}" = : ]; then
        directive=${lastLine[2,-1]}
        # Remove the directive including the : and the newline
        local suffix
        (( suffix=${#lastLine}+2))
        out=${out[1,-$suffix]}
    else
        # There is no directive specified.  Leave $out as is.
        __buf_debug "No directive found.  Setting do default"
        directive=0
    fi

    __buf_debug "directive: ${directive}"
    __buf_debug "completions: ${out}"
    __buf_debug "flagPrefix: ${flagPrefix}"

    if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
        __buf_debug "Completion received error. Ignoring completions."
        return
    fi

    compCount=0
    while IFS='\n' read -r comp; do
        if [ -n "$comp" ]; then
            # If requested, completions are returned with a description.
            # The description is preceded by a TAB character.
            # For zsh's _describe, we need to use a : instead of a TAB.
            # We first need to escape any : as part of the completion itself.
            comp=${comp//:/\\:}

            local tab=$(printf '\t')
            comp=${comp//$tab/:}

            ((compCount++))
            __buf_debug "Adding completion: ${comp}"
            completions+=${comp}
            lastComp=$comp
        fi
    done < <(printf "%s\n" "${out[@]}")

    if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
        # File extension filtering
        local filteringCmd
        filteringCmd='_files'
        for filter in ${completions[@]}; do
            if [ ${filter[1]} != '*' ]; then
                # zsh requires a glob pattern to do file filtering
                filter="\*.$filter"
            fi
            filteringCmd+=" -g $filter"
        done
        filteringCmd+=" ${flagPrefix}"

        __buf_debug "File filtering command: $filteringCmd"
        _arguments '*:filename:'"$filteringCmd"
    elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
        # File completion for directories only
        local subDir
        subdir="${completions[1]}"
        if [ -n "$subdir" ]; then
            __buf_debug "Listing directories in $subdir"
            pushd "${subdir}" >/dev/null 2>&1
        else
            __buf_debug "Listing directories in ."
        fi

        _arguments '*:dirname:_files -/'" ${flagPrefix}"
        if [ -n "$subdir" ]; then
            popd >/dev/null 2>&1
        fi
    elif [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ] && [ ${compCount} -eq 1 ]; then
        __buf_debug "Activating nospace."
        # We can use compadd here as there is no description when
        # there is only one completion.
        compadd -S '' "${lastComp}"
    elif [ ${compCount} -eq 0 ]; then
        if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
            __buf_debug "deactivating file completion"
        else
            # Perform file completion
            __buf_debug "activating file completion"
            _arguments '*:filename:_files'" ${flagPrefix}"
        fi
    else
        _describe "completions" completions $(echo $flagPrefix)
    fi
}