Bash – Argument Autocomplete in Bash

autocompletebash

I'm writing a bash to process files in a directory. I have some base (quite long) path in which to process. I want to specify via option and argument a sub-path and make it autocompletable. I mean something like this:

#!/bin/bash

dir=~/path/to/base/dir

while getopts ":d:" opt; do
    case $opt in
      d)
         dir=$dir/$OPTARG
         ;;
      #invalid input handling
    esac
done

But in this implementation the argument is not autocompletable and I have to type all the name of a sub-directory myself (Tab doesn't work).

Is there a way to achieve this in bash?

Best Answer

Yes, it is possible to implement path-completion from a custom base-directory specified by a command-line option-flag. Here is a small example illustrating how to accomplish this. First I'm going to slightly modify your example script to make the demonstration slightly more interesting (i.e. to produce output):

#!/bin/bash

# echo_path.sh
#
#    Echoes a path.
#

# Parse command-line options
while getopts ":d:" opt; do

    # Use '-d' to specify relative path
    case "${opt}" in
    d)
        directory="${OPTARG}"
        ;;
    esac
done

# Print the full path
echo "$(readlink -f ${directory})"

This is essentially the same as your example script, but it prints out the given argument.

Next we need to write a function to be called by the Bash programmatic-completion system. Here is a script which defines such a function:

# echo_path_completion.bash

# Programmatic path completion for user specified file paths.

# Define a base directory for relative paths.
export BASE_DIRECTORY=/tmp/basedir

# Define the completion function
function _echo_path_completion() {

    # Define local variables to store adjacent pairs of arguments
    local prev_arg;
    local curr_arg;

    # If there are at least two arguments then we have a candidate
    # for path-completion, i.e. we need the option flag '-d' and
    # the path string that follows it.
    if [[ ${#COMP_WORDS[@]} -ge 2 ]]; then

        # Get the current and previous arguments from the command-line
        prev_arg="${COMP_WORDS[COMP_CWORD-1]}";
        curr_arg="${COMP_WORDS[COMP_CWORD]}";

        # We only want to do our custom path-completion if the previous
        # argument is the '-d' option flag
        if [[ "${prev_arg}" = "-d" ]]; then

            # We only want to do our custom path-completion if a base
            # directory is defined and the argument is a relative path
            if [[ -n "${BASE_DIRECTORY}" && ${curr_arg} != /* ]]; then

                # Generate the list of path-completions starting from BASE_DIRECTORY
                COMPREPLY=( $(compgen -d -o default -- "${BASE_DIRECTORY}/${curr_arg}") );

                # Don't append a space after the command-completion
                # This is so we can continue to apply completion to subdirectories
                compopt -o nospace;

                # Return immediately
                return 0;
            fi
        fi
    fi

    # If no '-d' flag is given or no base directory is defined then apply default command-completion
    COMPREPLY=( $(compgen -o default -- "${curr_arg}") );
    return 0;
}

# Activate the completion function
complete -F _echo_path_completion echo_path

Now let's source our completion script:

source echo_path_completion.bash

Let's make our script executable and move it to somewhere in our PATH:

chmod +x echo_path.bash
mv -i echo_path.bash /usr/local/bin

And finally, let's add an alias for our script which doesn't have a file extension:

alias echo_path=echo_path.bash

Now if you enter echo -d and hit the tab button then you should get file-path completion starting from the BASE_DIRECTORY. To test this out you could try the following:

mkdir -p ${BASE_DIRECTORY}
mkdir -p "${BASE_DIRECTORY}/file"{1..5}

When you hit tab you should get the following list of completions:

user@host:~$ echo_path -d
user@host:~$ echo_path -d /tmp/basedir/file
/tmp/basedir/file1  /tmp/basedir/file2  /tmp/basedir/file3  /tmp/basedir/file4  /tmp/basedir/file5

Notice that after the first tab the string is converted to an absolute path. You can change this if you want, but I think this is might be the preferable behavior.

Here are some references you can consult for more information.

For official references, take a look at following sections of the Bash manual:

Also look at the Advanced Bash Scripting Guide from the Linux Documentation Project:

For a quick introduction to some features of programmatic completion in Bash, see this post from "The Geek Stuff" site:

There are also several related StackOverflow posts that you might find useful: