Creating a “cd” Wrapper with Bash Autocomplete

Abass Sesay
5 min readFeb 7, 2021
Bash terminal

TL;DR

A little bash function that wraps “cd” using a specified folder as a base folder complete with bash autocomplete.

Why

In the past year, I have been actively working on HackTheBox labs and now and then I find myself switch between directories in my “/HTB” . I keep having to use “cd ../<path>” or the full path. This is a minor inconvenience but I want a better way to do it; something like “<command> <path>”. Here “<command>” will know what know the base directory for “<path>”.

How

The way went about solving my problem was to create a bash function that would take the path passed and concatenate that with the base directory. The output then passed to cd.

function htb {
mcd "/home/kali/HTB/$@"
}

I used $@ here instead of $1 to give the flexibility to create folders with space in the name. I hardly every do this but it was a corner case I wanted to handle.

You might be thinking, what is this mcd ?

This is yet another wrapper for cd ;I got inspired by this article. This wrapper will create the directory if it does not exist. I threw a confirmation prompt before creating the directory. It is also doing a Sed substitution; changing + to space, the reason will be more apparent as later.

function mcd {
local path=$(sed 's/+/\ /g' <<< $@)
if [ ! -e "$path" ];
then
read -p "'$path' does not exist and will be created (Y[Enter]/N): " confirmation
case "$confirmation" in
[!yY]) return ;;
esac
fi
mkdir -p "$path"
cd "$path"
}

At this point we have functional wrapper with a base directory. The only things missing is the nice tab completion/suggestion that cd has.

There are built-in bash commands that can implement autocomplete. I used compgen and complete .

compgen -d will list all directories in the current directory. If you specify a directory, it will list all directories in that directory.

kali@kali:~/HTB$ compgen -d ~/HTB/
/home/kali/HTB/Academy
/home/kali/HTB/Admirer
/home/kali/HTB/Book
/home/kali/HTB/Bucket
/home/kali/HTB/Buff
/home/kali/HTB/Cache
/home/kali/HTB/Challenges
/home/kali/HTB/Compromised
/home/kali/HTB/Delivery
/home/kali/HTB/Doctor
/home/kali/HTB/Feline
/home/kali/HTB/HelperScripts
/home/kali/HTB/Jewel
...

complete [options] name specifics how argument are to be completed for name. name in my case is a function named htb . There are many options that can be specified for complete but in my case I only used 2; see link below for full documentation. I used the -o nospace flag, which controls the behavior of the completion by not adding a space after completion. I also used the -F flag to specify a function that updates the COMREPLY array variable that holds the auto complete option. This function is as follows.

function _comp_htb {
COMPREPLY=($(compgen -d /home/kali/HTB/"$2" |sed -r "s:\ :\\+:g"))
COMPREPLY=("${COMPREPLY[@]#/home/kali/HTB/}")

if [[ ${#COMPREPLY[@]} -eq 1 ]];
then
COMPREPLY=("${COMPREPLY[@]}/")
fi
}

Let’s go over what this function is doing.

$(compgen -d /home/kali/HTB/"$2" |sed -r "s:\ :\\+:g")

In the above command, compgenwill generate a list of directories that match the pattern passed. $2 in this case is the argument that the user is trying to autocomplete.

If you try to create a bash array with this output you will run into issues if the there are directories with space in their name. Example, if you have a directory named “Test This”, both “Test” and “This” will be considered as separated entries for the array.

ali@kali:~/Bash$ ls -al
total 12
drwxr-xr-x 3 kali kali 4096 Feb 6 18:27 ./
drwxr-xr-x 47 kali kali 4096 Feb 6 18:27 ../
drwxr-xr-x 2 kali kali 4096 Feb 6 18:27 'This Directory Name Has Spaces'/
kali@kali:~/Bash$ TEST=("$(compgen -d )")
printf 'Element -> %s\n' ${TEST[@]}
Element -> This
Element -> Directory
Element -> Name
Element -> Has
Element -> Spaces
kali@kali:~/Bash$

To bypass this issue, I used sed to replaced spaces with + and later when mcd is called, it will replace the + with space. The result is stored in COMPREPLY

Compreply — An array variable from which Bash reads the possible completions generated by a shell function invoked by the programmable completion facility (see Programmable Completion). Each array element contains one possible completion.

${COMPREPLY[@]#/home/kali/HTB/}

Here I am using shell parameter expansion to remove the base directory name from the list of autocomplete options. [@] traverses over the list and # deletes the pattern that follows; in this case /home/kali/HTB/ .

if [[ ${#COMPREPLY[@]} -eq 1 ]];
then
COMPREPLY=("${COMPREPLY[@]}/")
fi

This condition checks to determine if only a single directory has been resolved and from here on the autocomplete continues into that directory.

Putting everything together.

function mcd {
local path=$(sed 's/+/\ /g' <<< $@)
if [ ! -e "$path" ];
then
read -p "'$path' does not exist and will be created (Y[Enter]/N): " confirmation
case "$confirmation" in
[!yY]) return ;;
esac
fi
mkdir -p "$path"
cd "$path"
}
function htb {
mcd "/home/kali/HTB/$@"
}
function _comp_htb {
COMPREPLY=($(compgen -d /home/kali/HTB/"$2" |sed -r "s:\ :\\+:g"))
COMPREPLY=("${COMPREPLY[@]#/home/kali/HTB/}")

if [[ ${#COMPREPLY[@]} -eq 1 ]];
then
COMPREPLY=("${COMPREPLY[@]}/")
fi
}
complete -o nospace -F _comp_htb htb

What’s Next?

At this point, all that was promised in the title has been fulfilled. Despite this, I took it one step further. I do TryHackLabs every now and then so I replicated this solution for it. Instead of creating a special completion function for TryHackMe, I just abstracted my current completion function to use a different base directory depending on the function making the call.

function mcd {
local path=$(sed 's/+/\ /g' <<< $@)
if [ ! -e "$path" ];
then
read -p "'$path' does not exist and will be created (Y[Enter]/N): " confirmation
case "$confirmation" in
[!yY]) return ;;
esac
fi
mkdir -p "$path"
cd "$path"
}
function _comp_custom_cd {
local basedir
case $1 in
htb) basedir="/home/kali/HTB/" ;;
thm) basedir="/home/kali/TryHackMe/" ;;
esac
COMPREPLY=($(compgen -d "$basedir$2" |sed -r "s:\ :\\+:g"))
COMPREPLY=("${COMPREPLY[@]#$basedir}")

if [[ ${#COMPREPLY[@]} -eq 1 ]];
then
COMPREPLY=("${COMPREPLY[@]}/")
fi
}
function htb {
mcd "/home/kali/HTB/$@"
}
complete -o nospace -F _comp_custom_cd htbfunction thm {
mcd "/home/kali/TryHackMe/$1"
}
complete -o nospace -F _comp_custom_cd thm

Links

--

--

Abass Sesay

Aspiring Cybersecurity professional currently working as a Software Engineer. Hail from Sierra Leone currently living in the Pacific North West.