2
\$\begingroup\$

Ages ago I made a small bash script which would add a command cdp, standing for "change directory - project".

What it did was simple, it added all folders in ~/projects as autocompletion and CD'd me into them using only the project folder's name.

Over time I switched to a different company where I started working with Python, thus needing virtual environments not to mess up my workstation, which led me to adding more functionalities to the script.

I rarely use bash to do anything, rather using Python or even PHP for simple CLI tasks. I was hoping for some input regarding my first ever bash script, though, so I can keep my terminal scripts in its native language.

I think some improvements could be the way I name and call functions, and the newly added command should probably be something other than an alias.

#!/usr/bin/env bash

funcCheckVirtualEnvironment()
{
    if [ -d "venv/" ]; then
        read -p "A virtual environment was found for this project, would you like to execute the activation script? [y/n]" -n 1 -r
        echo
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            source venv/bin/activate
            if [ -e "requirements.txt" ]; then
                read -p "PyPi requirements were found, would you like to install any uninstalled packages? [y/n]" -n 1 -r
                echo
                if [[ $REPLY =~ ^[Yy]$ ]]; then
                    pip install -r requirements.txt
                fi
            fi  
        fi
    fi      
}

funcCDP()
{

    # Add more flags in future, maybe
    case "$1" in
    "-r" )
        funcCheckVirtualEnvironment
        return 0 ;;
    esac

    if [ -d ~/projects/$1 ]; then
        cd ~/projects/$1;
        funcCheckVirtualEnvironment
    else
        echo "Directory $1 does not exist!"
    fi

}

funcCD()
{
    if [ $1 == "-"] || [ -d $1 ]; then
        command cd "$@"
        if [ -d "venv/" ]; then
            funcCheckVirtualEnvironment
        fi
    else
        echo "No such file or directory: $1"
    fi
}

_cdp()
{
        local cur=${COMP_WORDS[COMP_CWORD]}
        COMPREPLY=( $(compgen -W '$(\ls ~/projects)' -- $cur) )
}
complete -F _cdp cdp
alias cdp=funcCDP
alias cd=funcCD
\$\endgroup\$
  • 1
    \$\begingroup\$ Have you tried virtualenvwrapper for python? It has an awesome feature setvirtualenvproject which binds any project/directory to an existing/active virtualenv; and later you can cdproject to switch to that. \$\endgroup\$ – hjpotter92 Mar 26 '18 at 14:02
  • \$\begingroup\$ @hjpotter92 I haven't and will try it out at home, though I am reluctant to use a different virtual environment manager at work than the rest of my co-workers. Thanks for the suggestion! \$\endgroup\$ – Berry M. Mar 26 '18 at 14:03
  • \$\begingroup\$ virtualenvwrapper is a friendlier (read humane) version for virtualenv. \$\endgroup\$ – hjpotter92 Mar 26 '18 at 14:06
1
\$\begingroup\$

No shebang

It doesn't make sense to execute this as a program - it's intended to be sourced into an interactive shell. So we shouldn't have the #! line.

Quote expansions

$1 could expand into multiple words here:

    if [ -d ~/projects/$1 ]; then
        cd ~/projects/$1;

and here:

    if [ $1 == "-"] || [ -d $1 ]; then

Send errors to stderr

    echo "Directory $1 does not exist!" >&2
    #                                   ^^^ IMPORTANT

Don't ask questions when used non-interactively

funcCheckVirtualEnvironment can misbehave when standard input is not a terminal. We should test that case and return early:

test -r "venv/bin/activate" || return 0;
if ! test -t 0
then echo "Warning: not activating virtual environment" >&2
     return 0   # or 1 if this should be a failure
fi
read -p "Execute this project's activation script? [y/N]" -n 1 -r; echo
[[ REPLY =~ [Yy] ]] || return 0;

I've transformed some of the other if and else above into early returns, to reduce the nesting depth - I find that easier to read. Also, I've capitalised N in the prompt, to provide the usual indication of the default. And we know that $REPLY is a single character, so we can lose the anchors.

Let cd do its own checking instead

There's a possible race between checking for the existence of a directory and actually changing into it, so it's easier and safer to simply check whether cd succeeded:

command cd "$@" && test -d 'venv' && funcCheckVirtualEnvironment

This also allows cd to produce the error message (in the user's preferred $LANGuage).

Consider the exit status of the command

If we cd but there's no venv directory at the destination, that should be considered a success:

command cd "$@" || return $?
if test -d "venv"
then funcCheckVirtualEnvironment
else true
fi

Actually, there's no need to check for the existence of venv/, given that funcCheckVirtualEnvironment does that itself, and should succeed if it's not present:

command cd "$@" && funcCheckVirtualEnvironment

Similarly, in cdp -r, instead of return 0, a plain return is better, as that will result in the return value of the last executed command (funcCheckVirtualEnvironment in this case).

Use standard directory completion

It's bad practice to parse the output of ls. Instead, we can ask compgen to give us directory names; we can eliminate absolute paths and subdirectories by excluding anything containing /:

_cdp()
{
        COMPREPLY=( $(cd ~/projects && compgen -X '*/*' -d $2) )
}

My version

funcCheckVirtualEnvironment()
{
    test -r 'venv/bin/activate' || return 0;
    if ! test -t 0
    then
        # Not a terminal - don't ask questions
        echo "Warning: not activating virtual environment" >&2
        return 0   # or 1 if this should be a failure
    fi

    read -p "Execute this project's activation script? [y/N]" -n 1 -r; echo
    [[ REPLY =~ [Yy] ]] || return 0;
    source venv/bin/activate || return $?

    test -e 'requirements.txt' || return 0;
    read -p "Install any missing PyPi packages? [y/N]" -n 1 -r; echo
    [[ REPLY =~ [Yy] ]] || return 0;
    pip install -r requirements.txt
}

funcCDP()
{
    # Add more flags in future, maybe
    case "$1" in
    "-r" )
        funcCheckVirtualEnvironment
        return
        ;;
    esac

    cd ~/projects/"$1" && funcCheckVirtualEnvironment
}

funcCD()
{
    command cd "$@" && funcCheckVirtualEnvironment
}

_cdp()
{
    COMPREPLY=( $(cd ~/projects && compgen -X '*/*' -d $2) )
}
complete -F _cdp cdp
alias cdp=funcCDP
alias cd=funcCD
\$\endgroup\$
  • \$\begingroup\$ Thanks a bunch! Quite a few of these tips will be very helpful in future bash scripts. I never knew of the test command, and going off of it's manpage I'll be using it a lot! \$\endgroup\$ – Berry M. Mar 27 '18 at 11:41
  • 1
    \$\begingroup\$ @Berry - I think you did already know about test (you have already been using it under its other name, [). It's merely a matter of preference whether you call it test or [...]. \$\endgroup\$ – Toby Speight Apr 2 '18 at 10:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.