bash expand cd with shortcuts like zsh

前端 未结 2 1045
清歌不尽
清歌不尽 2021-01-02 18:15

Is it possible in bash to expand something like

cd /u/lo/b

to

cd /usr/local/bin

?

相关标签:
2条回答
  • 2021-01-02 18:49

    I have come up with an alternative solution that does not break existing bash completion rules in other places.

    The idea is to append a wildcard (asterisk) to every element of the path and invoke normal bash completion process from there. So when user types /u/lo/b<Tab> my function substitutes that with /u*/lo*/b* and invokes bash completion as usual.

    To enable the described behavior source this file from your ~/.bashrc. Supported features are:

    • Special characters in completed path are automatically escaped if present
    • Tilde expressions are properly expanded (as per bash documentation)
    • If user had started writing the path in quotes, no character escaping is applied. Instead the quote is closed with a matching character after expanding the path.
    • If bash-completion package is already in use, this code will safely override its _filedir function. No extra configuration is required.

    Watch a demo screencast to see this feature in action:

    Full code listing below (you should check the GitHub repo for latest updates though):

    #!/usr/bin/env bash
    #
    # Zsh-like expansion of incomplete file paths for Bash.
    # Source this file from your ~/.bashrc to enable the described behavior.
    #
    # Example: `/u/s/a<Tab>` will be expanded into `/usr/share/applications`
    #
    
    
    # Copyright 2018 Vitaly Potyarkin
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    
    
    
    
    #
    # Take a single incomplete path and fill it with wildcards
    # e.g. /u/s/app/ -> /u*/s*/app*
    #
    _put_wildcards_into_path() {
        local PROCESSED TILDE_EXPANSION
        PROCESSED=$( \
            echo "$@" | \
            sed \
                -e 's:\([^\*\~]\)/:\1*/:g' \
                -e 's:\([^\/\*]\)$:\1*:g' \
                -e 's:\/$::g' \
                -e 's:^\(\~[^\/]*\)\*\/:\1/:' \
                -Ee 's:(\.+)\*/:\1/:g' \
        )
        eval "TILDE_EXPANSION=$(printf '%q' "$PROCESSED"|sed -e 's:^\\\~:~:g')"
        echo "$TILDE_EXPANSION"
    }
    
    
    #
    # Bash completion function for expanding partial paths
    #
    # This is a generic worker. It accepts 'file' or 'directory' as the first
    # argument to specify desired completion behavior
    #
    _complete_partial() {
        local WILDCARDS ACTION LINE OPTION INPUT UNQUOTED_INPUT QUOTE
    
        ACTION="$1"
        if [[ "_$1" == "_-d" ]]
        then  # _filedir compatibility
            ACTION="directory"
        fi
        INPUT="${COMP_WORDS[$COMP_CWORD]}"
    
        # Detect and strip opened quotes
        if [[ "${INPUT:0:1}" == "'" || "${INPUT:0:1}" == '"' ]]
        then
            QUOTE="${INPUT:0:1}"
            INPUT="${INPUT:1}"
        else
            QUOTE=""
        fi
    
        # Add wildcards to each path element
        WILDCARDS=$(_put_wildcards_into_path "$INPUT")
    
        # Collect completion options
        COMPREPLY=()
        while read -r -d $'\n' LINE
        do
            if [[ "_$ACTION" == "_directory" && ! -d "$LINE" ]]
            then  # skip non-directory paths when looking for directory
                continue
            fi
            if [[ -z "$LINE" ]]
            then  # skip empty suggestions
                continue
            fi
            if [[ -z "$QUOTE"  ]]
            then  # escape special characters unless user has opened a quote
                LINE=$(printf "%q" "$LINE")
            fi
            COMPREPLY+=("$LINE")
        done <<< $(compgen -G "$WILDCARDS" "$WILDCARDS" 2>/dev/null)
        return 0  # do not clutter $? value (last exit code)
    }
    
    
    # Wrappers
    _complete_partial_dir() { _complete_partial directory; }
    _complete_partial_file() { _complete_partial file; }
    
    # Enable enhanced completion
    complete -o bashdefault -o default -o nospace -D -F _complete_partial_file
    
    # Optional. Make sure `cd` is autocompleted only with directories
    complete -o bashdefault -o default -o nospace -F _complete_partial_dir cd
    
    # Override bash-completion's _filedir (if it's in use)
    # https://salsa.debian.org/debian/bash-completion
    _filedir_original_code=$(declare -f _filedir|tail -n+2)
    if [[ ! -z "$_filedir_original_code" ]]
    then
        eval "_filedir_original() $_filedir_original_code"
        _filedir() {
            _filedir_original "$@"
            _complete_partial "$@"
        }
    fi
    
    # Readline configuration for better user experience
    bind 'TAB:menu-complete'
    bind 'set colored-completion-prefix on'
    bind 'set colored-stats on'
    bind 'set completion-ignore-case on'
    bind 'set menu-complete-display-prefix on'
    bind 'set show-all-if-ambiguous on'
    bind 'set show-all-if-unmodified on'
    
    0 讨论(0)
  • 2021-01-02 19:10

    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

    0 讨论(0)
提交回复
热议问题