cat /dev/brain

Showing bash some love

So in my last post I discussed my pyserv bash function that looks and behaves like a program. I didn't exactly disect it, but I have a different one that I will disect.

Meet my nifty function called sandbox:

sandbox(){
    if [[ -z "$1" ]] ; then
        echo "sandbox: No directory provided"
    else
        home="$HOME/sandbox/"
        mnt=""

        if [[ -d "$MNTPT" ]] && [[ -d "$MNTPT/sandbox/" ]] ; then
            mnt="$MNTPT/sandbox/"
        fi

        if [[ -d $mnt$1 ]] ; then
            cd $mnt$1
        elif [[ -d $home$1 ]] ; then
            cd $home$1
        else
            echo "sandbox: \"$1\" does not exist"
        fi
    fi
}

How the function works

Everything is 0-indexed, and arguments to a function or program are no different. $0 is the name of the function or program being called in bash. So the start of the legitimate arguments is conveniently $1. [[ and ]] perform tests on expressions and -z happens to be a special test: whether the string is of zero-length or not.

The assigments to home and mnt should be self-explanatory which leaves us with two more tests. They both use this flag -d which tests if the path provided is a directory that exists.

Finally, we test if the path of either sandbox concatenated with the first argument exists and is a directory. If it is, we cd into it and we're done. Otherwise, we echo our error and continue on.

Why?

I have a lot of git repositories and I like to store them in ~/sandbox, but that started to become very cluttered. Running cd ~/sandbox/g[TAB] was starting to become a hassle. My initial solution was to start using a spare harddrive as a mount point and putting my less used directories in there. The problem then became remembering where I had which directory. Not only did I feel like a genius for devising this plan, I felt even more intelligent being able to remember where I had my repositories. (That was sarcasm in case you didn't catch that.)

So my initial solution was to write sandbox. Nice now I have a function that will know what I'm talking about so long as the names are distinct (and yes, I guarantee you all of my repository names are distinct). But now a new problem reared its ugly head. Now I have to type out the entire directory name that I want to cd into. Well that becomes more and more fun every time you do it. (Hopefully by now you can identify sarcasm.)

Back to good old cd, right? Wrong. Bash allows for users to specify how they want it to complete parameters to a command. So, I took advantage of that. (I like the ability to fall back on old habits and easily cd into a directory in ~/sandbox, so I'm leaving the other repositories on the harddrive.)

Completion is typically (from what I've seen) done via either functions or files. I did this as another bash function since all of this is going into my ~/.bash_profile which is tracked in my private dotfiles repository. The function looks exactly like:

_sandbox(){
    local cur list

    COMREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    list=""
    if [[ $COMP_CWORD -eq 1 ]] ; then
        if [[ -d "$localsb" ]] ; then
            for f in $localsb/* ; do
                if [[ -d "$f" ]] ; then
                    list+=("$(basename $f)")
                fi
            done
        fi

        mntsb="$MNTPT/sandbox"
        if [[ -d "$mntsb" ]] ; then
            for f in $mntsb/* ; do
                if [[ -d "$f" ]] ; then
                    list+=("$(basename $f)")
                fi
            done
        fi
    fi

    COMPREPLY=( $(compgen -W "$list" -- "$cur") )
    return 0
}

What does this do?

local declares the words separated by spaces following it to be variables only available in the context of the function. We then initialize COMREPLY to an empty array. This is the variable that bash will require for completion to work. Then we initialize cur to the word we're trying to complete and list to an empty string.

Next we have another test. This time we're seeing if COMPT_CWORD is equal to 1. Why? Well I only want completion to work once. I don't want to be able to complete multiple directories or words when I don't need to. If in fact this is the first argument to sandbox, then I'm going to note the current directory for convenience, and cd into the other directory to list its directories. Then if the mount point exists we'll do the same and finally move back to our original directory.

Finally, we fill COMPREPLY with an array made by compgen. This will give us all the completions we desire when we hit Tab.

Since I first wrote this ...

... I have changed the name to sbx instead of sandbox. Why? Well as you can tell I usually have one directory in $HOME called sandbox/ and that was interfering with tab-completion for the name. It also avoids conflict with sb which I don't use, but I may in the future.

In Conclusion

Bash completion is something that is non-obvious but will teach you a bit about how your command-line works. If you want to see some more (or less) complex examples, check out /etc/bash_completion.d/. It is full of completion rules for different tools and programs (e.g., ssh). I found this to be fun, but then again whenever I learn something new I enjoy it, so don't take my word for it.