Creating a "cd" Wrapper with Bash Autocomplete
Tags: bash, productivity, scripting
Published: 2021-02-07 02:39:59



# 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 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.
```bash
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.
```bash
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. 
```bash
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.
```bash
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.
```bash
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/` .
```bash
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.
```bash
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.
```bash
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.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