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
1 Answer 1
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 $LANG
uage).
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
-
\$\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.– Berry M.2018年03月27日 11:41:47 +00:00Commented Mar 27, 2018 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 ittest
or[
...]
. \$\endgroup\$Toby Speight– Toby Speight2018年04月02日 10:17:12 +00:00Commented Apr 2, 2018 at 10:17
virtualenvwrapper
for python? It has an awesome featuresetvirtualenvproject
which binds any project/directory to an existing/active virtualenv; and later you cancdproject
to switch to that. \$\endgroup\$virtualenvwrapper
is a friendlier (read humane) version forvirtualenv
. \$\endgroup\$