Search code examples
bashpathexpandcd

bash expand cd with shortcuts like zsh


Is it possible in bash to expand something like

cd /u/lo/b<hit tab>

to

cd /usr/local/bin

?


Solution

  • Sorry I couldn't post earlier, I was held at work, and the bind function was more issue-prone than I first thought.

    Here is what I came up with :

    Bind the following script :

    #!/bin/bash
    #$HOME/.bashrc.d/autocomplete.sh
    autocomplete_wrapper() {
        BASE="${READLINE_LINE% *} "           #we save the line except for the last argument
        [[ "$BASE" == "$READLINE_LINE " ]] && BASE="";  #if the line has only 1 argument, we set the BASE to blank
        EXPANSION=($(autocomplete "${READLINE_LINE##* }"))
        [[ ${#EXPANSION[@]} -gt 1 ]] && echo "${EXPANSION[@]:1}"  #if there is more than 1 match, we echo them
        READLINE_LINE="$BASE${EXPANSION[0]}"  #the current line is now the base + the 1st element
        READLINE_POINT=${#READLINE_LINE}      #we move our cursor at the end of the current line
    }
    
    autocomplete() {
        LAST_CMD="$1"
        #Special starting character expansion for '~', './' and '/'
        [[ "${LAST_CMD:0:1}" == "~" ]] && LAST_CMD="$HOME${LAST_CMD:1}"
        S=1; [[ "${LAST_CMD:0:1}" == "/" || "${LAST_CMD:0:2}" == "./" ]] && S=2; #we don't expand those
    
        #we do the path expansion of the last argument here by adding a * before each /
        EXPANSION=($(echo "$LAST_CMD*" | sed s:/:*/:"$S"g))
    
        if [[ ! -e "${EXPANSION[0]}" ]];then #if the path cannot be expanded, we don't change the output
            echo "$LAST_CMD"
        elif [[ "${#EXPANSION[@]}" -eq 1 ]];then #else if there is only one match, we output it
            echo "${EXPANSION[0]}"
        else
            #else we expand the path as much as possible and return all the possible results
            while [[ $l -le "${#EXPANSION[0]}" ]]; do
                for i in "${EXPANSION[@]}"; do
                    if [[ "${EXPANSION[0]:$l:1}" != "${i:$l:1}" ]]; then
                        CTRL_LOOP=1
                        break
                    fi
                done
                [[ $CTRL_LOOP -eq 1 ]] && break
                ((l++))
            done
            #we add the partial solution at the beggining of the array of solutions
            echo "${EXPANSION[0]:0:$l} ${EXPANSION[@]}"
        fi
    }
    

    with the following command :

        source "$HOME/.bashrc.d/autocomplete.sh" 
        bind -x '"\t" : autocomplete_wrapper'
    

    Output :

    >$ cd /u/lo/b<TAB>
    >$ cd /usr/local/bin
    
    
    >$ cd /u/l<TAB>
    /usr/local /usr/lib
    >$ cd /usr/l
    

    The bind line could be added to your ~/.bashrc file, doing something like this :

    if [[ -s "$HOME/.bashrc.d/autocomplete.sh" ]]; then
        source "$HOME/.bashrc.d/autocomplete.sh" 
        bind -x '"\t" : autocomplete_wrapper'
    fi
    

    (taken from this answer)

    Furthermore, I would strongly advise against binding this command to your Tab key as it would override the default autocomplete.

    Note: In some cases, this will misbehave, for isntance if you try to autocomplete "/path/with spaces/something", as the last argument to complete is determined by ${READLINE_LINE##* }. If this is an issue in your case, you should code a function that returns the last argument of a line when considering quotes

    Feel free to ask for further clarification, and I welcome any suggestion to improve this script