Skip to content

Instantly share code, notes, and snippets.

@the-glima
Forked from leolorenzoluis/Bash best practices
Created March 24, 2021 11:12
Show Gist options
  • Save the-glima/39905476c82a8acf9c81b7cfe08e5f03 to your computer and use it in GitHub Desktop.
Save the-glima/39905476c82a8acf9c81b7cfe08e5f03 to your computer and use it in GitHub Desktop.
# Bash best practices and style-guide
Just simple methods to keep the code clean.
Inspired by [progrium/bashstyle](https://github.com/progrium/bashstyle) and [Kfir Lavi post](http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming/).
## Quick big rules
* All code goes in a function
* Always double quote variables
* Avoid global variables and declare them as `readonly`
* Always have a `main()` function for runnable scripts
* Always use `set -eo pipefail` : fail fast and be aware of exit codes
* Define functions as `myfunc() { ... }`, not `function myfun {...}`
* Always use `[[` instead of `[` or `test`
* Use `$( ... )` instead of backticks
* Prefer absolute paths and always qualify relative paths with `./.`
* Warnings and errors should go to `STDERR`, anything parsable should go to `STDOUT`
* Use `.sh` or `.bash` extension if file is meant to be included or sourced
## More specific rules with some example
### Global variables
* Avoid global vars
* Always UPPER_CASE naming
* Readonly declaration
* Globals that can be always use in any program :
```
readonly PROGNAME=$(basename $0)
readonly PROGDIR=$(readlink -m $(dirname $0))
readonly ARGS="$@"
```
### Other variables
* All variables should be local (they can only be used in functions)
* Always lowercase naming
* Self documenting parameters
```
fn_example() {
local explicit_name = $1 ;
local expName = $1 ;
}
```
* Usually use `i` for loop, so it is very important to declare it as local
### Main()
* Use always a `main()` function
* The only global command in the code is : `main` or `main "$@"`
* If script is also usable as library, call it using `[[ "$0" == "$BASH_SOURCE" ]] && main "$@"`
### Everything is a function
* Only the `main()` function and global declarations are run globaly
* Short code portion can be functions
* Define functions as `myfunc() { ... }`, not `function myfun {...}`
### Debugging
* Run with -x flag : `bash -x prog.sh`
* Debug just a small section of code using set -x and set +x
* Printing function name and its arguments `echo $FUNCNAME $@`
### Each line of code does just one thing
* Break expression with back slash `\`
* Use symbols at the start of the indented line
```
print_dir_if_not_empty() {
local dir=$1
is_empty $dir \
&& echo "dir is empty" \
|| echo "dir=$dir"
}
```
### Command line arguments
```
cmdline() {
local arg=
for arg
do
local delim=""
case "$arg" in
#translate --gnu-long-options to -g (short options)
--config) args="${args}-c ";;
--pretend) args="${args}-n ";;
--test) args="${args}-t ";;
--help-config) usage_config && exit 0;;
--help) args="${args}-h ";;
--verbose) args="${args}-v ";;
--debug) args="${args}-x ";;
#pass through anything else
*) [[ "${arg:0:1}" == "-" ]] || delim="\""
args="${args}${delim}${arg}${delim} ";;
esac
done
#Reset the positional parameters to the short options
eval set -- $args
while getopts "nvhxt:c:" OPTION
do
case $OPTION in
v)
readonly VERBOSE=1
;;
h)
usage
exit 0
;;
x)
readonly DEBUG='-x'
set -x
;;
t)
RUN_TESTS=$OPTARG
verbose VINFO "Running tests"
;;
c)
readonly CONFIG_FILE=$OPTARG
;;
n)
readonly PRETEND=1
;;
esac
done
if [[ $recursive_testing || -z $RUN_TESTS ]]; then
[[ ! -f $CONFIG_FILE ]] \
&& eexit "You must provide --config file"
fi
return 0
}
```
### Unit Testing
* Very important in higher level languages
* Use [shunit2](https://shunit2.googlecode.com/svn/trunk/source/2.1/doc/shunit2.html) for unit testing
* Good intro to shunit2 : [shUnit2 - Bash Testing](http://www.mikewright.me/blog/2013/10/31/shunit2-bash-testing/)
* Another good ressource : [Test Driving Shell Scripts](http://code.tutsplus.com/tutorials/test-driving-shell-scripts--net-31487)
* The list of current assertions (as of version 2.1.6) :
* `assertEquals [message] expected actual`
* `assertSame [message] expected actual`
* `assertNotEquals [message] expected actual`
* `assertNotSame [message] expected actual`
* `assertNull [message] value` # used to compare a null in bash which is a zero length string
* `assertNotNull [message] value` # used to compare a null in bash which is a zero length string
* `assertTrue [message] condition`
* `assertFalse [message] condition`
* The list of current failures (do not use them for value comparisons, use assertions for this) :
* `fail [message]`
* `failNotEquals [message] unexpected actual`
* `failSame [message] expected actual`
* `failNotSame [message] unexpected actual`
* More specific functions :
* `setUp` : run automatically before each test
* `tearDown` run automatically after each test
* `|| startSkipping` automatically skip after a test failure (default is to continue)
### Usefull links and good references
* Obsolete and deprecated bash syntax :
[http://wiki.bash-hackers.org/scripting/obsolete](http://wiki.bash-hackers.org/scripting/obsolete)
* Beginner mistakes [http://wiki.bash-hackers.org/scripting/newbie_traps](http://wiki.bash-hackers.org/scripting/newbie_traps)
* Advanced Bash-Scripting Guide [http://tldp.org/LDP/abs/html/](http://tldp.org/LDP/abs/html/)
* Google's Bash styleguide [http://google-styleguide.googlecode.com/svn/trunk/shell.xml](http://google-styleguide.googlecode.com/svn/trunk/shell.xml)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment