I recently worked on creating a Bash completion script for a project, and I enjoyed it very much. In this post, I will try to familiarize you with the process of creating a Bash completion script.

What is Bash completion?

Bash completion is a functionality through which Bash helps users type their commands more quickly and easily. It does this by presenting possible options when users press the Tab key while typing a command.

$ git < tab >< tab >

git git-receive-pack git-upload-archive

gitk git-shell git-upload-pack

$ git-s < tab >

$ git-shell

How it works

complete

Why bother?

The completion script is code that uses the builtin Bash commandto definecan be displayed for a given. The nature of the completion options vary, from simple static to highly sophisticated.

This functionality helps users by:

saving them from typing text when it can be auto-completed

helping them know the available continuations to their commands

preventing errors and improving their experience by hiding or showing options based on what they have already typed

Hands-on

Here’s what we will do in this tutorial:

We will first create a dummy executable script called dothis . All it does is execute the command that resides on the number that was passed as an argument in the user’s history. For example, the following command will simply execute the ls -a command, given that it exists in history with number 235 :

dothis 235

Then we will create a Bash completion script that will display commands along with their number from the user’s history, and we will “bind” it to the dothis executable.

$ dothis < tab >< tab >

215 ls

216 ls -la

217 cd ~

218 man history

219 git status

220 history | cut -c 8 -

You can see a gif demonstrating the functionality at this tutorial’s code repository on GitHub.

Let the show begin.

Creating the executable script

Create a file named dothis in your working directory and add the following code:

if [ -z "$1" ] ; then

echo "No command number passed"

exit 2

fi



exists =$ ( fc -l -1000 | grep ^ $1 -- 2 >/ dev / null )



if [ -n " $exists " ] ; then

fc -s -- "$1"

else

echo "Command with number $1 was not found in recent history"

exit 2

fi

Notes:

We first check if the script was called with an argument

We then check if the specific number is included in the last 1000 commands if it exists, we execute the command using the fc functionality otherwise, we display an error message



Make the script executable with:

chmod +x . / dothis

We will execute this script many times in this tutorial, so I suggest you place it in a folder that is included in your path so that we can access it from anywhere by typing dothis .

I installed it in my home bin folder using:

install . / dothis ~ / bin / dothis

You can do the same given that you have a ~/bin folder and it is included in your PATH variable.

Check to see if it’s working:

dothis

You should see this:

$ dothis

No command number passed

Done.

Creating the completion script

Create a file named dothis-completion.bash . From now on, we will refer to this file with the term completion script.

Once we add some code to it, we will source it to allow the completion to take effect. We must source this file every single time we change something in it.

Later in this tutorial, we will discuss our options for registering this script whenever a Bash shell opens.

Static completion

Suppose that the dothis program supported a list of commands, for example:

now

tomorrow

never

Let’s use the complete command to register this list for completion. To use the proper terminology, we say we use the complete command to define a completion specification (compspec) for our program.

Add this to the completion script.

#/usr/bin/env bash

complete -W "now tomorrow never" dothis

Here's what we specified with the complete command above:

we used the -W (wordlist) option to provide a list of words for completion.

(wordlist) option to provide a list of words for completion. we defined to which “program” these completion words will be used (the dothis parameter)

Source the file:

source . / dothis-completion.bash

Now try pressing Tab twice in the command line, as shown below:

$ dothis < tab >< tab >

never now tomorrow

Try again after typing the n :

$ dothis n < tab >< tab >

never now

Magic! The completion options are automatically filtered to match only those starting with n .

Note: The options are not displayed in the order that we defined them in the word list; they are automatically sorted.

There are many other options to be used instead of the -W that we used in this section. Most produce completions in a fixed manner, meaning that we don’t intervene dynamically to filter their output.

For example, if we want to have directory names as completion words for the dothis program, we would change the complete command to the following:

complete -A directory dothis

Pressing Tab after the dothis program would get us a list of the directories in the current directory from which we execute the script:

$ dothis < tab >< tab >

dir1 / dir2 / dir3 /

Find the complete list of the available flags in the Bash Reference Manual.

Dynamic completion

We will be producing the completions of the dothis executable with the following logic:

If the user presses the Tab key right after the command, we will show the last 50 executed commands along with their numbers in history

If the user presses the Tab key after typing a number that matches more than one command from history, we will show only those commands along with their numbers in history

If the user presses the Tab key after a number that matches exactly one command in history, we auto-complete the number without appending the command’s literal (if this is confusing, no worries—you will understand later)

Let’s start by defining a function that will execute each time the user requests completion on a dothis command. Change the completion script to this:

#/usr/bin/env bash

_dothis_completions ( )

{

COMPREPLY+= ( "now" )

COMPREPLY+= ( "tomorrow" )

COMPREPLY+= ( "never" )

}



complete -F _dothis_completions dothis

Note the following:

we used the -F flag in the complete command defining that the _dothis_completions is the function that will provide the completions of the dothis executable

flag in the complete command defining that the is the function that will provide the completions of the executable COMPREPLY is an array variable used to store the completions—the completion mechanism uses this variable to display its contents as completions

Now source the script and go for completion:

$ dothis < tab >< tab >

never now tomorrow

Perfect. We produce the same completions as in the previous section with the word list. Or not? Try this:

$ dothis nev < tab >< tab >

never now tomorrow

As you can see, even though we type nev and then request for completion, the available options are always the same and nothing gets completed automatically. Why is this happening?

The contents of the COMPREPLY variable are always displayed. The function is now responsible for adding/removing entries from there.

variable are always displayed. The function is now responsible for adding/removing entries from there. If the COMPREPLY variable had only one element, then that word would be automatically completed in the command. Since the current implementation always returns the same three words, this will not happen.

Enter compgen : a builtin command that generates completions supporting most of the options of the complete command (ex. -W for word list, -d for directories) and filtering them based on what the user has already typed.

Don’t worry if you feel confused; everything will become clear later.

Type the following in the console to better understand what compgen does:

$ compgen -W "now tomorrow never"

now

tomorrow

never

$ compgen -W "now tomorrow never" n

now

never

$ compgen -W "now tomorrow never" t

tomorrow

So now we can use it, but we need to find a way to know what has been typed after the dothis command. We already have the way: The Bash completion facilities provide Bash variables related to the completion taking place. Here are the more important ones:

COMP_WORDS : an array of all the words typed after the name of the program the compspec belongs to

: an array of all the words typed after the name of the program the belongs to COMP_CWORD : an index of the COMP_WORDS array pointing to the word the current cursor is at—in other words, the index of the word the cursor was when the tab key was pressed

: an index of the array pointing to the word the current cursor is at—in other words, the index of the word the cursor was when the tab key was pressed COMP_LINE : the current command line

To access the word just after the dothis word, we can use the value of COMP_WORDS[1]

Change the completion script again:

#/usr/bin/env bash

_dothis_completions ( )

{

COMPREPLY = ( $ ( compgen -W "now tomorrow never" " ${COMP_WORDS[1]} " ) )

}



complete -F _dothis_completions dothis

Source, and there you are:

$ dothis

never now tomorrow

$ dothis n

never now

Now, instead of the words now, tomorrow, never, we would like to see actual numbers from the command history.

The fc -l command followed by a negative number -n displays the last n commands. So we will use:

fc -l -50

which lists the last 50 executed commands along with their numbers. The only manipulation we need to do is replace tabs with spaces to display them properly from the completion mechanism. sed to the rescue.

Change the completion script as follows:

#/usr/bin/env bash

_dothis_completions ( )

{

COMPREPLY = ( $ ( compgen -W " $(fc -l -50 | sed 's/\t//') " -- " ${COMP_WORDS[1]} " ) )

}



complete -F _dothis_completions dothis

Source and test in the console:

$ dothis < tab >< tab >

632 source dothis-completion.bash 649 source dothis-completion.bash 666 cat ~ / .bash_profile

633 clear 650 clear 667 cat ~ / .bashrc

634 source dothis-completion.bash 651 source dothis-completion.bash 668 clear

635 source dothis-completion.bash 652 source dothis-completion.bash 669 install . / dothis ~ / bin / dothis

636 clear 653 source dothis-completion.bash 670 dothis

637 source dothis-completion.bash 654 clear 671 dothis 6546545646

638 clear 655 dothis 654 672 clear

639 source dothis-completion.bash 656 dothis 631 673 dothis

640 source dothis-completion.bash 657 dothis 150 674 dothis 651

641 source dothis-completion.bash 658 dothis 675 source dothis-completion.bash

642 clear 659 clear 676 dothis 651

643 dothis 623 ls -la 660 dothis 677 dothis 659

644 clear 661 install . / dothis ~ / bin / dothis 678 clear

645 source dothis-completion.bash 662 dothis 679 dothis 665

646 clear 663 install . / dothis ~ / bin / dothis 680 clear

647 source dothis-completion.bash 664 dothis 681 clear

648 clear 665 cat ~ / .bashrc

Not bad.

We do have a problem, though. Try typing a number as you see it in your completion list and then press the key again.

$ dothis 623 < tab >

$ dothis 623 ls 623 ls -la

...

$ dothis 623 ls 623 ls 623 ls 623 ls 623 ls -la

This is happening because in our completion script, we used the ${COMP_WORDS[1]} to always check the first typed word after the dothis command (the number 623 in the above snippet). Hence the completion continues to suggest the same completion again and again when the Tab key is pressed.

To fix this, we will not allow any kind of completion to take place if at least one argument has already been typed. We will add a condition in our function that checks the size of the aforementioned COMP_WORDS array.

#/usr/bin/env bash

_dothis_completions ( )

{

if [ " ${#COMP_WORDS[@]} " ! = "2" ] ; then

return

fi



COMPREPLY = ( $ ( compgen -W " $(fc -l -50 | sed 's/\t//') " -- " ${COMP_WORDS[1]} " ) )

}



complete -F _dothis_completions dothis

Source and retry.

$ dothis 623 < tab >

$ dothis 623 ls -la < tab > # SUCCESS: nothing happens here

There is another thing we don’t like, though. We do want to display the numbers along with the corresponding commands to help users decide which one is desired, but when there is only one completion suggestion and it gets automatically picked by the completion mechanism, we shouldn’t append the command literal too.

In other words, our dothis executable accepts only a number, and we haven’t added any functionality to check or expect other arguments. When our completion function gives only one result, we should trim the command literal and respond only with the command number.

To accomplish this, we will keep the response of the compgen command in an array variable, and if its size is 1, we will trim the one and only element to keep just the number. Otherwise, we’ll let the array as is.

Change the completion script to this:

#/usr/bin/env bash

_dothis_completions ( )

{

if [ " ${#COMP_WORDS[@]} " ! = "2" ] ; then

return

fi



# keep the suggestions in a local variable

local suggestions = ( $ ( compgen -W " $(fc -l -50 | sed 's/\t/ /') " -- " ${COMP_WORDS[1]} " ) )



if [ " ${#suggestions[@]} " == "1" ] ; then

# if there's only one match, we remove the command literal

# to proceed with the automatic completion of the number

local number =$ ( echo ${suggestions[0]/%\ */} )

COMPREPLY = ( " $number " )

else

# more than one suggestions resolved,

# respond with the suggestions intact

COMPREPLY = ( " ${suggestions[@]} " )

fi

}



complete -F _dothis_completions dothis

Registering the completion script

If you want to enable the completion just for you on your machine, all you have to do is add a line in your .bashrc file sourcing the script:

source < path-to-your-script >/ dothis-completion.bash

If you want to enable the completion for all users, you can just copy the script under /etc/bash_completion.d/ and it will automatically be loaded by Bash.

Fine-tuning the completion script

Here are some extra steps for better results:

Displaying each entry in a new line

In the Bash completion script I was working on, I too had to present suggestions consisting of two parts. I wanted to display the first part in the default color and the second part in gray to distinguish it as help text. In this tutorial’s example, it would be nice to present the numbers in the default color and the command literal in a less fancy one.

Unfortunately, this is not possible, at least for now, because the completions are displayed as plain text and color directives are not processed (for example: \e[34mBlue ).

What we can do to improve the user experience (or not) is to display each entry in a new line. This solution is not that obvious since we can’t just append a new line character in each COMPREPLY entry. We will follow a rather hackish method and pad suggestion literals to a width that fills the terminal.

Enter printf . If you want to display each suggestion on each own line, change the completion script to the following:

#/usr/bin/env bash

_dothis_completions ( )

{

if [ " ${#COMP_WORDS[@]} " ! = "2" ] ; then

return

fi



local IFS =$ '

'

local suggestions = ( $ ( compgen -W " $(fc -l -50 | sed 's/\t//') " -- " ${COMP_WORDS[1]} " ) )



if [ " ${#suggestions[@]} " == "1" ] ; then

local number = " ${suggestions[0]/%\ */} "

COMPREPLY = ( " $number " )

else

for i in " ${!suggestions[@]} " ; do

suggestions [ $i ] = " $(printf '%*s' "-$COLUMNS" "${suggestions[$i]}") "

done



COMPREPLY = ( " ${suggestions[@]} " )

fi

}



complete -F _dothis_completions dothis

Source and test:

dothis < tab >< tab >

...

499 source dothis-completion.bash

500 clear

...

503 dothis 500

Customizable behavior

In our case, we hard-coded to display the last 50 commands for completion. This is not a good practice. We should first respect what each user might prefer. If he/she hasn’t made any preference, we should default to 50.

To accomplish that, we will check if an environment variable DOTHIS_COMPLETION_COMMANDS_NUMBER has been set.

Change the completion script one last time:

#/usr/bin/env bash

_dothis_completions ( )

{

if [ " ${#COMP_WORDS[@]} " ! = "2" ] ; then

return

fi



local commands_number = ${DOTHIS_COMPLETION_COMMANDS_NUMBER:-50}

local IFS =$ '

'

local suggestions = ( $ ( compgen -W " $(fc -l -$commands_number | sed 's/\t//') " -- " ${COMP_WORDS[1]} " ) )



if [ " ${#suggestions[@]} " == "1" ] ; then

local number = " ${suggestions[0]/%\ */} "

COMPREPLY = ( " $number " )

else

for i in " ${!suggestions[@]} " ; do

suggestions [ $i ] = " $(printf '%*s' "-$COLUMNS" "${suggestions[$i]}") "

done



COMPREPLY = ( " ${suggestions[@]} " )

fi

}



complete -F _dothis_completions dothis

Source and test:

export DOTHIS_COMPLETION_COMMANDS_NUMBER = 5

$ dothis < tab >< tab >

505 clear

506 source . / dothis-completion.bash

507 dothis clear

508 clear

509 export DOTHIS_COMPLETION_COMMANDS_NUMBER = 5

Useful links

Code and comments

You can find the code of this tutorial on GitHub.

For feedback, comments, typos, etc., please open an issue in the repository.

Long post, cat photo

Let me introduce you to my debugger:

That’s all, folks!

This post was originally posted at Iridakos.com. Reposted with permission.