DEV Community

Cover image for Creating a "cd" Wrapper with Bash Autocomplete
Abass Sesay
Abass Sesay

Posted on

Creating a "cd" Wrapper with Bash Autocomplete

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/$@"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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$
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Links

https://www.digitalocean.com/community/tutorials/an-introduction-to-useful-bash-aliases-and-functions

https://www.gnu.org/software/bash/manual/bash.html#index-COMPREPLY

https://www.gnu.org/software/bash/manual/bash.html#Programmable-Completion-Builtins

Top comments (3)

Collapse
 
nishithsavla profile image
Nishith Savla

Why don't you use FISH? It has built-in autocompletion and better, more intuitive scripting syntax
Give FISH a try. Maybe it will solve your problems

Collapse
 
bascoe10 profile image
Abass Sesay

I tried FISH a while back but some reason I stuck with bash/zsh. I might give it another go.

Collapse
 
nishithsavla profile image
Nishith Savla

Ohh. Maybe you'll like it in another try