# Small bash script to enhance virtual environments on directory navigation

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

• 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. – hjpotter92 Mar 26 '18 at 14:02
• @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! – Berry M. Mar 26 '18 at 14:03
• virtualenvwrapper is a friendlier (read humane) version for virtualenv. – hjpotter92 Mar 26 '18 at 14:06

# 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

• 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! – Berry M. Mar 27 '18 at 11:41
• @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 [...]. – Toby Speight Apr 2 '18 at 10:17