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

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

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

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs