Creating a "cd" Wrapper with Bash Autocomplete
Abass Sesay
Posted on February 7, 2021
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 ../" or the full path. This is a minor inconvenience but I want another way to do it; something like <command> <path>
. Here command
will know what know the base directory for the 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, compgen
will 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.
kali@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. I store the result 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, I have fulfilled all that I promised in the title. 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 htb
function thm {
mcd "/home/kali/TryHackMe/$1"
}
complete -o nospace -F _comp_custom_cd thm
Links
https://www.gnu.org/software/bash/manual/bash.html#index-COMPREPLY
https://www.gnu.org/software/bash/manual/bash.html#Programmable-Completion-Builtins
Posted on February 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.