One of the great features of the bash shell is autocompletion. It is most widely used for file system path completion. It saves a ton of typing when navigating the file system. Type <tab><tab> and get a list of files and directories. Type a few characters and type <tab> and BOOM no more typing required. In this post, I’ll show how I enabled autocomplete for a custom shell function.

I’ve recently been writing a few shell functions to automate some of my recurring tasks. These functions are to help me be more efficient and productive. But, having to remember the valid values for the arguments and then type them out each time was less than ideal. I had never researched the options to enabling autocompletion for custom scripts or functions before, but it turns out that it is very simple.

The example function here is used to archive a project directory. It requires the project name (i.e. directory name) as an argument. Even though file system autocompletion automatically works in bash, it only works based on the current working directory or requires that you provide the path plus the directory or filename. Since all of my projects are in the same parent directory and I want to run this function from anywhere on the file system, I needed more targeted autocompletion.

This is a slimmed down version of my function.

arprj() {
   PARENT_DIR=/tmp/Projects
   PROJECT=$1
   PROJECT_DIR=${PARENT_DIR}/${PROJECT}
   ARCHIVE_DIR=${PARENT_DIR}/Archive
   
   mv ${PROJECT_DIR} ${ARCHIVE_DIR}
}

Now, for autocompletion, I need to be able to get a list of directories contained in the parent directory. And, I need to exclude the Archive directory from that list.

Bash has a three built-in functions that allow you to specify autocompletion rules for arbitrary commands. They are complete, compgen, and compopt. You can find more information about these in the bash man page. I’m just going to talk about complete in this post. I toyed briefly with incorporating compgen in the solution, but got stuck trying to get wildcards to work.

My first approach was to call complete using the -W option. When using -W, you provide a word list that is used for the autocomple options. Here is what that looked like.

complete -W "`echo $(ls /tmp/Projects | sed 's/\/$//g' | grep -v '^Archive$')`" arprj

A command is run to provide the -W argument with the contents of the parent directory in a space separated string. The trailing slash (/) is removed from each directory name and the Archive directory is excluded. This worked well. It provided the list of directories for autocompletion. But, it has one problem. The list of projects is static. Once this command is run (normaly in your shell start up scripts e.g. .bashrc), new projects added to the parent directory do not get added to the list. I had to find a better way.

The -F argument to the complete command gave me what I needed. Using -F, you provide a function to be called that will generate the list real time.

_project_list() {
    local cur
    
    # Initialize an empty list
    COMPREPLY=()
    
    # Get the text from the command line to autocomplete
    cur="${COMP_WORDS[COMP_CWORD]}"
    
    # Produce a list of options based on the supplied text
    COMPREPLY=( $(ls  /tmp/Projects | sed 's/\/$//g' | grep -v '^Archive$' | grep i "^${cur}.*"))
}

complete -F _project_list arprj

The same basic command is used to produce the list, but now it is inside the function, I am using grep to limit the results in the list. As it is, it does a ‘begins with’ search but there’s also an added benefit. I can use any regular expression on the command line that I can use with grep and my autocomplete results will be based on that expression. Extra keystrokes are also saved by including the -i option to grep for case-insensitive matching.

Here is the final setup in action.

Custom Autocomplete in Bash

Custom Autocomplete in Bash

There are probably a lot more powerful things you can do with bash’s autocomplete functions. I’d love to hear how you have used this to save keystrokes and become more efficient. Please share in the comments.

Share this on: