Search code examples
bashautocompletebash-completion

Bash Completion how to ask it not to append a SPACE after current word is completed


In bash by default, pressing Tab will show all files and directories in the current directory. For example:

cat a<tab><tab> would display something like aFile.txt apples.png aDirectory/

If you then completed to aDirectory, it will show the contents of aDirectory, for example:

cat aDirectory/<tab><tab> might display file.txt file.mp4

It does this while keeping the cursor on the current argument. So pressing <tab><tab> on cat aD would end with the cursor like this cat aDirectory/| (where the pipe is the cursor)

I've been trying to make my own bash completion scripts, and one is for a log program. This program is broken down into months in individual files. So 2308, 2309 etc. What I want is that when I press <tab><tab>, until 4 characters are filled, it will display only the year and months, so 2308 2309, but when that is filled (log 2308|), pressing <tab><tab> would then show the individual days in the file (so 230801 230803 230807 for example).

I managed to get this working, however the issue is if tab autocompletes (for example if the only options were 2308, 2309 and 2310, then hitting <tab> on 231 would autocomplete to 2310), the cursor jumps to the next argument (log 2310 | instead of log 2310|) unlike the default behaviour with directories.

Then of course pressing tab does nothing, as it is no longer on ${COMP_WORDS[1]}. How can you get the cursor to not jump to the next argument, until some condition is met (e.g. ${COMP_WORDS[1]} is a file, or ${COMP_WORDS[1]} is 6 characters long)?

This is the full function:

_logCompletion() {
    if [ "${#COMP_WORDS[@]}" != "2" ]; then
        if [ "${COMP_WORDS[1]}" == "-d" ] && [ "${#COMP_WORDS[@]}" == "3" ]; then # If -d was chosen, then display the avalible years/months
            if [ ${#COMP_WORDS[2]} -ge 4 ]; then # If month already filled, start showing avalible days
                curLogMonth=${COMP_WORDS[2]:0:4} # Get month in yymm format
                [ -f ~/Programs/output/log/${curLogMonth}.json ] || return 0 # Check that month exists, if not, don't continue
                curMonthSuggestions=$(cat ~/Programs/output/log/$curLogMonth.json | jq -r 'keys[]' | sed "s/\(.*\)/$curLogMonth\1/g") # Get the days avalible in given month
                COMPREPLY=( $(compgen -W "$curMonthSuggestions" -- "${COMP_WORDS[2]}") )
                return 0
            else # Otherwise just show avalible months
                COMPREPLY=( $(compgen -W "$(ls ~/Programs/output/log/ | grep -oE [0-9]*)" -- "${COMP_WORDS[2]}") )
                return 0
            fi
        elif [ "${COMP_WORDS[1]}" == "-ds" ] && [ "${#COMP_WORDS[@]}" == "3" ]; then # If -ds was chosen, show avalible months
            COMPREPLY=( $(compgen -W "$(ls ~/Programs/output/log/ | grep -oE [0-9]*)" -- "${COMP_WORDS[2]}") )
        fi
        return 0
    fi
    COMPREPLY=( $(compgen -W "-h -p -d -ds -s -f -fa" -- "${COMP_WORDS[1]}") )
}

Solution

  • Option #1

    complete -o nospace -F _logCompletion log
    

    This way it would even not automatically append a SPACE when your date is already 6-digits long.

    Option #2

    Update the else ... block like this:

    else # Otherwise just show avalible months
        COMPREPLY=( $(compgen -W ... ) )
        if [[ ${#COMPREPLY[@]} == 1 ]]; then
            COMPREPLY[1]="${COMPREPLY[0]}/"
            #                            ^ (other char is also fine)
        fi
        return 0
    fi
    

    This way yourself can control when you want it to auto append a SPACE.