56

I have a bunch of bash completion scripts set up (mostly using bash-it and some manually setup).

I also have a bunch of aliases setup for common tasks like gco for git checkout. Right now I can type git checkout dTab and develop is completed for me but when I type gco dTab it does not complete.

I'm assuming this is because the completion script is completing on git and it fails to see gco.

Is there a way to generically/programmatically get all of my completion scripts to work with my aliases? Not being able to complete when using the alias kind of defeats the purpose of the alias.

dstarh
  • 685
  • 1
  • 6
  • 10
  • What OS and bash are you using? I am on Ubuntu 11.10 and bash 4.2.10(1)-release (x86_64-pc-linux-gnu) and I have this functionaity built in to my shell for my many aliases. btw `bash --version` to get this (don't use `-v`, different output). – Michael Durrant Jun 16 '12 at 13:14
  • Sorry missed taht bit of info - OSX Lion, GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin11) – dstarh Jun 18 '12 at 13:10
  • 1
    @killermist: unless I’m completely mistaken, zsh doesn’t complete aliased commands out of the box either. Implementing a function that adds defined aliases to completion appears to be much easier than for bash, though, as zhs’s completion system seems both more powerful and more straightforward than bash’s. – kopischke Jun 23 '12 at 19:22
  • Cross site duplicate: http://stackoverflow.com/questions/342969/how-do-i-get-bash-completion-to-work-with-aliases/1793178 – Ciro Santilli OurBigBook.com Jan 20 '14 at 08:57
  • @ciro I agree that the question is a bit dupe but IMO this answer addresses the Subject of the question, not JUST the example in the question. – dstarh Jan 20 '14 at 14:03
  • @dstarh do you mean that that question is focused on specific example (`git checkout`) while yours is about general tab completion for all aliases? If this is it I disagree since the other question says "Ideally I'd like autocompletion to just magically work for all my aliases", so it seems he wants a general solution for any program. Also no `git` tag either there. – Ciro Santilli OurBigBook.com Jan 20 '14 at 14:40
  • @ciro No, the title of the SO question is "How do I get bash completion to work with aliases?", the title for this question is "How can I get bash to perform tab-completion for my aliases?". The selected answer for the SO question answers the example whereas the selected answer here answers the title, so I feel this is a better answer regardless of it being a duplicate or not. – dstarh Jan 20 '14 at 15:30
  • 1
    @MichaelDurrant Are you sure that this is actually built in for aliases? I am on Ubuntu 15.10 with Bash 4.3.42(1)-release (x86_64-pc-linux-gnu) and there is no such thing. I also tested a few earlier releases. So for example if you type in `ll --[TAB]` it will print a list of options for `ls`? I'm pretty skeptical of this, but if you're sure such a thing existed in 11.10 I'd be curious to dig through it and determine what was removed. – Six Nov 23 '15 at 14:49

5 Answers5

54

The following code, adapted from this Stack Overflow answer and this Ubuntu Forums discussion thread will add completions for all your defined aliases:

# Automatically add completion for all aliases to commands having completion functions
function alias_completion {
    local namespace="alias_completion"

    # parse function based completion definitions, where capture group 2 => function and 3 => trigger
    local compl_regex='complete( +[^ ]+)* -F ([^ ]+) ("[^"]+"|[^ ]+)'
    # parse alias definitions, where capture group 1 => trigger, 2 => command, 3 => command arguments
    local alias_regex="alias ([^=]+)='(\"[^\"]+\"|[^ ]+)(( +[^ ]+)*)'"

    # create array of function completion triggers, keeping multi-word triggers together
    eval "local completions=($(complete -p | sed -Ene "/$compl_regex/s//'\3'/p"))"
    (( ${#completions[@]} == 0 )) && return 0

    # create temporary file for wrapper functions and completions
    rm -f "/tmp/${namespace}-*.tmp" # preliminary cleanup
    local tmp_file; tmp_file="$(mktemp "/tmp/${namespace}-${RANDOM}XXX.tmp")" || return 1

    local completion_loader; completion_loader="$(complete -p -D 2>/dev/null | sed -Ene 's/.* -F ([^ ]*).*/\1/p')"

    # read in "<alias> '<aliased command>' '<command args>'" lines from defined aliases
    local line; while read line; do
        eval "local alias_tokens; alias_tokens=($line)" 2>/dev/null || continue # some alias arg patterns cause an eval parse error
        local alias_name="${alias_tokens[0]}" alias_cmd="${alias_tokens[1]}" alias_args="${alias_tokens[2]# }"

        # skip aliases to pipes, boolean control structures and other command lists
        # (leveraging that eval errs out if $alias_args contains unquoted shell metacharacters)
        eval "local alias_arg_words; alias_arg_words=($alias_args)" 2>/dev/null || continue
        # avoid expanding wildcards
        read -a alias_arg_words <<< "$alias_args"

        # skip alias if there is no completion function triggered by the aliased command
        if [[ ! " ${completions[*]} " =~ " $alias_cmd " ]]; then
            if [[ -n "$completion_loader" ]]; then
                # force loading of completions for the aliased command
                eval "$completion_loader $alias_cmd"
                # 124 means completion loader was successful
                [[ $? -eq 124 ]] || continue
                completions+=($alias_cmd)
            else
                continue
            fi
        fi
        local new_completion="$(complete -p "$alias_cmd")"

        # create a wrapper inserting the alias arguments if any
        if [[ -n $alias_args ]]; then
            local compl_func="${new_completion/#* -F /}"; compl_func="${compl_func%% *}"
            # avoid recursive call loops by ignoring our own functions
            if [[ "${compl_func#_$namespace::}" == $compl_func ]]; then
                local compl_wrapper="_${namespace}::${alias_name}"
                    echo "function $compl_wrapper {
                        (( COMP_CWORD += ${#alias_arg_words[@]} ))
                        COMP_WORDS=($alias_cmd $alias_args \${COMP_WORDS[@]:1})
                        (( COMP_POINT -= \${#COMP_LINE} ))
                        COMP_LINE=\${COMP_LINE/$alias_name/$alias_cmd $alias_args}
                        (( COMP_POINT += \${#COMP_LINE} ))
                        $compl_func
                    }" >> "$tmp_file"
                    new_completion="${new_completion/ -F $compl_func / -F $compl_wrapper }"
            fi
        fi

        # replace completion trigger by alias
        new_completion="${new_completion% *} $alias_name"
        echo "$new_completion" >> "$tmp_file"
    done < <(alias -p | sed -Ene "s/$alias_regex/\1 '\2' '\3'/p")
    source "$tmp_file" && rm -f "$tmp_file"
}; alias_completion

For simple (command only, no arguments) aliases it will assign the original completion function to the alias; for aliases with arguments, it creates a wrapper function that inserts the extra arguments into the original completion function.

Unlike the scripts it has evolved from, the function respects quotes both for the alias command and its arguments (but the former have to be matched by the completion command, and cannot be nested), and it should reliably filter out aliases to command lists and pipes (which are skipped, as it is impossible to find out what to complete in them without re-creating the complete shell command line parsing logic).

Usage

Either save the code as a shell script file and source that in, or copy the function wholesale into, .bashrc (or your pertinent dot file). The important thing is to call the function after both bash completion and alias definitions have been set up (the code above calls the function right after its definition, in a “source and forget” spirit, but you can move the call anywhere downstream if that suits you better). If you don’t want the function in your environment after it exits, you can add unset -f alias_completion after calling it.

Notes

If you are using bash 4.1 or above and use dynamically-loaded completions, the script will attempt to load completions for all of your aliased commands so that it can build the wrapper functions for your aliases.

kopischke
  • 2,226
  • 19
  • 27
  • 1
    How would I go about *installing* that script? – Oliver Salzburg Jun 16 '12 at 10:45
  • 1
    @OliverSalzburg: you’ll have to process it in one of your shell profile files, crucially **after** bash completion – that would probably make it `~/.bashrc`. Either store it as a shell script file and source it (`. /path/to/alias_completion.sh`), or copy and paste the code wholesale. – kopischke Jun 16 '12 at 12:04
  • 1
    @OliverSalzburg: added usage instructions (didn’t notice right away you are not the OP). – kopischke Jun 16 '12 at 12:27
  • As would likely have been pertinent info this is on mac osx and is not working for me. – dstarh Jun 18 '12 at 13:23
  • @dstarh: so am I – the function works fine with the same configuration as you have on my machine. How exactly is it not working for you (try calling the function in the shell after it has been sourced, that should show the error messages, if any)? – kopischke Jun 18 '12 at 13:43
  • thats how I tested it. no error messages but gco tab does not give me my list of branches – dstarh Jun 18 '12 at 13:52
  • also tried just my git alias which is g, then g che tab does not complete either - figured gco might have been a problem since it's already git checkout – dstarh Jun 18 '12 at 13:54
  • @dstarh: what is the output of `complete -p | grep git`? – kopischke Jun 18 '12 at 14:17
  • `complete -o bashdefault -o default -o nospace -F _git git` `complete -o bashdefault -o default -o nospace -F _gitk gitk` – dstarh Jun 18 '12 at 21:27
  • https://gist.github.com/2951959 - my git aliases – dstarh Jun 19 '12 at 02:23
  • i should note that ecom $BASH_COMPLETION returns nothing on my mac – dstarh Jun 19 '12 at 03:19
  • let us [continue this discussion in chat](http://chat.stackexchange.com/rooms/3808/discussion-between-kopischke-and-dstarh) – kopischke Jun 19 '12 at 06:05
  • This script isn't working for me. I placed it in my .aliases, and when I source it, there are no problems. But when I attempt to use the completion, it prints `bash: COMP_POINT - : syntax error: operand expected (error token is "- ")` then returns. Ubuntu 12.04, default bash. – ssmy Oct 24 '12 at 00:38
  • @ssmy: could you provide me with a list of your aliases (`alias -p`) – and defined completions prior to sourcing the script (`complete -p`) preferably as a downloadable text file somewhere, to avoid cluttering the comment thread? – kopischke Oct 24 '12 at 20:57
  • @kopischke Aliases: http://paste.ubuntu.com/1305484/ Completions pre-script: http://paste.ubuntu.com/1305486/ Thanks for the help. – ssmy Oct 25 '12 at 18:11
  • @ssmy: which command triggers the error on completion? – kopischke Oct 25 '12 at 23:05
  • @kopischke `install`, the alias for `sudo apt-get install` – ssmy Oct 26 '12 at 00:48
  • @kopischke Your script is not working for me though I have included it as `source ~/.bash_aliases_completions` at the very end of the `~/.bashrc` file. For install I am not getting completion of `apt-cache policy` despite having `alias acp='apt-cache policy'` in `~/.bash_aliases`. Trying to debug I find that on line 31, `${completions[*]}` does not contain the command `apt-cache` at all though the relevant file `/usr/share/bash-completion/completions/apt-cache` does exist. Please help. – jamadagni Oct 25 '14 at 14:59
  • 1
    @kopischke See [this question](http://stackoverflow.com/questions/15858271/bash-completion-completion-function-defined-the-first-time-a-command-is-invoke) -- apparently for the files under `/usr/share/bash-completion/completions/` they are loaded only the first time the user actually hits `[TAB]`. So even if the function is loaded from `~/.bashrc` it will not generate completions for aliases to commands therein. After ensuring `complete -p` is working for `apt-get` and `apt-cache` I copy-pasted your function to the terminal and it is working correctly. – jamadagni Oct 25 '14 at 15:44
  • 1
    @kopischke So I'm not sure how to force sourcing of all the dynamically loaded completion files, or even if it is advisable. For now I have copied the generated completion file from `/tmp` to `~/.bash_completion` and manually added at its beginning the relevant `source /usr/share/bash-completion/completions/` entries (separately for `apt-get` and `apt-cache` -- `apt-{cache,get}` doesn't work). – jamadagni Oct 25 '14 at 15:52
  • A final word: a `sed "s/sudo //g"` would be useful for kopischke's script -- else it's unlikely it would do the correct thing for sudo-ed commands. See my comment [here](http://stackoverflow.com/questions/342969/how-do-i-get-bash-completion-to-work-with-aliases/1793178#comment41743634_1793178) – jamadagni Oct 25 '14 at 15:58
  • @jamadagni honestly, I have no idea either. I wasn’t even aware of `bash`’s 4 dynamic completion loading feature until your comment, nor do I have access to it (I’m on `bash` 3.2 until someone sorts out the GPL3 vs. Apple mess). I added a disclaimer to the answer and that is about what I can do. – kopischke Oct 27 '14 at 11:38
  • 1
    The '-E' flag to sed won't work on older GNU sed - you need to use the '-r' flag that it is an alias for. [Apparently](http://blog.dmitryleskov.com/small-hacks/mysterious-gnu-sed-option-e/) the '-E' flag was added for BSD compatibility. – aaren Nov 15 '14 at 11:44
  • In the [current sed source](http://git.savannah.gnu.org/cgit/sed.git/tree/sed/sed.c?id=ef7eab74e3a89583ff523fb5fd501920cc9b1645#n128) the '-E' flag is indicated as the portable POSIX option. – aaren Nov 15 '14 at 11:54
  • This is awesome! I've added this as a plugin to the Bash-it framework here: https://github.com/Bash-it/bash-it/blob/master/plugins/available/alias-completion.bash – nwinkler Sep 07 '15 at 10:37
6

Is there a way to generically/programmatically get all of my completion scripts to work with my aliases?

Yes, here is the complete-alias project which solves your problem exactly. It provides generic and programmatic alias completion without using eval.

Cyker
  • 331
  • 6
  • 18
  • This seems to still be maintained in 2022, and I can confirm works very well. Thanks for sharing this! [Here](https://github.com/twpayne/chezmoi/discussions/2408#discussioncomment-3908443) is a description of how I use it. – Lockszmith Oct 18 '22 at 17:50
6

This is the manual way, for those that are looking for this.

First, look up the original completion command. Example:

$ complete | grep git

complete -o bashdefault -o default -o nospace -F __git_wrap__git_main git

Now add these to your startup script (e.g. ~/.bashrc):

# load dynamically loaded completion functions (may not be required)
_completion_loader git

# copy the original statement, but replace the last command (git) with your alias (g)
complete -o bashdefault -o default -o nospace -F __git_wrap__git_main g

source: https://superuser.com/a/1004334

wisbucky
  • 2,928
  • 31
  • 29
  • If there is no `complete` output for a particular command when using `grep`, then you have to perform a tab completion on the command first. – Snake Feb 28 '21 at 22:08
1

kopischke did a great job in figuring out the completion wrapping (thanks!).

However, with the "right" aliases, code can be executed unwantedly, which could lead to potential data loss:

$ unalias -a
$ alias cleantmp='echo Removed `rm -v /tmp/uniquenameasdf* | wc -l` files.'
$ touch /tmp/uniquenameasdf42
$ alias_completion 
$ rm /tmp/uniquenameasdf42
rm: cannot remove '/tmp/uniquenameasdf42': No such file or directory
$

eval is dangerous!

  • Good catch. That's why you have @Cyker's [answer](https://superuser.com/a/1159801/100730), which doesn't use eval, and still works. – Lockszmith Oct 18 '22 at 17:52
1

The answer given previously by kopischke works like a charm on Debian 10 only if you delete the alias_completion at the end

Here is the correct snippet to paste in your ~/.bashrc:

# Automatically add completion for all aliases to commands having completion functions
function alias_completion {
    local namespace="alias_completion"

    # parse function based completion definitions, where capture group 2 => function and 3 => trigger
    local compl_regex='complete( +[^ ]+)* -F ([^ ]+) ("[^"]+"|[^ ]+)'
    # parse alias definitions, where capture group 1 => trigger, 2 => command, 3 => command arguments
    local alias_regex="alias ([^=]+)='(\"[^\"]+\"|[^ ]+)(( +[^ ]+)*)'"

    # create array of function completion triggers, keeping multi-word triggers together
    eval "local completions=($(complete -p | sed -Ene "/$compl_regex/s//'\3'/p"))"
    (( ${#completions[@]} == 0 )) && return 0

    # create temporary file for wrapper functions and completions
    rm -f "/tmp/${namespace}-*.tmp" # preliminary cleanup
    local tmp_file; tmp_file="$(mktemp "/tmp/${namespace}-${RANDOM}XXX.tmp")" || return 1

    local completion_loader; completion_loader="$(complete -p -D 2>/dev/null | sed -Ene 's/.* -F ([^ ]*).*/\1/p')"

    # read in "<alias> '<aliased command>' '<command args>'" lines from defined aliases
    local line; while read line; do
        eval "local alias_tokens; alias_tokens=($line)" 2>/dev/null || continue # some alias arg patterns cause an eval parse error
        local alias_name="${alias_tokens[0]}" alias_cmd="${alias_tokens[1]}" alias_args="${alias_tokens[2]# }"

        # skip aliases to pipes, boolean control structures and other command lists
        # (leveraging that eval errs out if $alias_args contains unquoted shell metacharacters)
        eval "local alias_arg_words; alias_arg_words=($alias_args)" 2>/dev/null || continue
        # avoid expanding wildcards
        read -a alias_arg_words <<< "$alias_args"

        # skip alias if there is no completion function triggered by the aliased command
        if [[ ! " ${completions[*]} " =~ " $alias_cmd " ]]; then
            if [[ -n "$completion_loader" ]]; then
                # force loading of completions for the aliased command
                eval "$completion_loader $alias_cmd"
                # 124 means completion loader was successful
                [[ $? -eq 124 ]] || continue
                completions+=($alias_cmd)
            else
                continue
            fi
        fi
        local new_completion="$(complete -p "$alias_cmd")"

        # create a wrapper inserting the alias arguments if any
        if [[ -n $alias_args ]]; then
            local compl_func="${new_completion/#* -F /}"; compl_func="${compl_func%% *}"
            # avoid recursive call loops by ignoring our own functions
            if [[ "${compl_func#_$namespace::}" == $compl_func ]]; then
                local compl_wrapper="_${namespace}::${alias_name}"
                    echo "function $compl_wrapper {
                        (( COMP_CWORD += ${#alias_arg_words[@]} ))
                        COMP_WORDS=($alias_cmd $alias_args \${COMP_WORDS[@]:1})
                        (( COMP_POINT -= \${#COMP_LINE} ))
                        COMP_LINE=\${COMP_LINE/$alias_name/$alias_cmd $alias_args}
                        (( COMP_POINT += \${#COMP_LINE} ))
                        $compl_func
                    }" >> "$tmp_file"
                    new_completion="${new_completion/ -F $compl_func / -F $compl_wrapper }"
            fi
        fi

        # replace completion trigger by alias
        new_completion="${new_completion% *} $alias_name"
        echo "$new_completion" >> "$tmp_file"
    done < <(alias -p | sed -Ene "s/$alias_regex/\1 '\2' '\3'/p")
    source "$tmp_file" && rm -f "$tmp_file"
};
roneo.org
  • 186
  • 2