## Writing Good Bash Scripts v2021 [Mirek Biňas](https://bletvaska.github.io) / [****]()
## Intro * I am not an enthusiastic _bash ~~stripter~~ scripter_ * bash is not suitable for every problem * it's great tool for piping of different solutions/tools * works as **glue** * it's **swiss army knife** of every _DevOps_ * following list is not complete
## #1 Don't forget about portability!
### Portable `Hello world!` ```bash #!/usr/bin/env bash echo "Hello world!" ``` * `env` - run a program in a modified environment * looks for interpreter in `$PATH`
## #2 Stop on Error!
### Wrong Script ```bash #!/usr/bin/env bash echo "start here" VAR = "syntax error on this line" echo "this line will be executed too" ```
### Start This Way ```bash #!/usr/bin/env bash set -o errexit # stop when error occurs set -o pipefail # if not, expressions like `error here | true` # will always succeed set -o nounset # detects uninitialised variables set -o xtrace # prints every expression # before executing it (debugging) echo "start here" VAR = "syntax error on this line" echo "this line is now not reachable" ```
## #3 Enclose Vars as `"${variable}"`
* _escaping_ * the `'"'` helps with variables with white spaces ```bash $ track="with or without you.mp3" $ file $track with: cannot open `with' (No such file or directory) or: cannot open `or' (No such file or directory) without: cannot open `without' (No such file or directory) you.mp3: cannot open `you.mp3' (No such file or directory) ```
* the `'{}'` are not necessary, but are used in _bash expansion_: * string interpolation: ```bash "${variable}.yml" ``` * default (fallback) value: ```bash "${variable:-something_else}" ``` * string replacement: ```bash "${variable//from/to}" ```
## #4 Use `[[ expr ]]` instead of `[ expr ]` note: * `[[` - extension of bash * `[` - short for `test`
## Cool Features I. * no need to enclose variables into quotes ```bash [[ -f ${file} ]] # vs. [ -f "${file}" ] [[ $var = "joe" ]] # vs. [ $var = "joe" ] ``` * support for operators `"&&"` and `"||"` ```bash [[ ${USERNAME} == "root" && $(id -g) == 1 ]] ``` * string comparision with `"<"` and `">"` ```bash [[ "apple" < "banana" ]] ``` note: ```bash [ $var = "joe" ] ```
## Cool Features II. * operator `"=~"` for regex comparisions ```bash [[ ${answer} =~ ^y(es)?$ ]] ``` * supports _globbing_ ```bash [[ ${answer} = y* ]] ```
## #5 Use Modular Approach
* write scripts in modules (not a single file) * easy maintenance * easy navigation * modules are separated by functionality * script can be extended with new functionality with `source` ```bash # ... source "${DATADIR}/helper.sh" # ... ```
## Detect Sourcing * detect, if the script is being run directly, or if it is just being sourced ```bash # call the func only if the script is executed directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main fi ``` * script can be executed normally, but if it's sourced, only the function definitions will be imported without doing anything else note: * https://advancedweb.hu/unit-testing-bash-scripts/
## #6 Don't Use Tools whose Functions `bash` Handles
* one of the most common (unconscious) mistakes * most common `grep`, `awk`, `sed`, `bc`, `expr`, e.g.: ```bash size=$(du -hs images/ | awk '{print $1}') ``` can be solved with bash as ```bash array=($(du -hs images/)) size=${array[0]} ``` * instead of `seq 1 10` use `{1..10}` * use `$(cmd)` instead of `cmd`
## #7 Write Scripts so That They can be Tested
* close everything to the function (approach) ```bash #!/usr/bin/env bash main(){ echo "Hello world!" } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main fi ``` * many tools for testing in bash are available (e.g. [bats](https://github.com/sstephenson/bats)) ```bash #!/usr/bin/env bats @test "when new folder is created, then exit status is 0" { run mkdir "${dir}" assert_equal "${status}" 0 } ```
## #8 Use Linter
> **Lint** or a **linter**, is a static code analysis tool used to flag programming errors, bugs, stylistic errors and suspicious constructs. It helps to increase the quality of code. > > -- [Wikipedia](https://en.wikipedia.org/wiki/Linter_%28software%29)
### [ShellCheck](https://www.shellcheck.net/) * Shell script analysis tool * contains [Gallery of bad code](https://github.com/koalaman/shellcheck/blob/master/README.md#user-content-gallery-of-bad-code) * shows, what kind of things does ShellCheck look for * in your Linux distro
## #9 Use Variable Annotations
* `readonly` - for readonly (constant) variables ```bash readonly var='immutable value' ``` * `local` - for local variables inside of functions ```bash _greetings(){ local name="$1" printf "Hello %s!\n" "${name}" } ```
## #10 Avoid Temporary Files
* some commands expect filenames as params * pipelining does not work * operator `<()` ```bash jq .main.temp <(http https://openweathermap.org) ``` * _here documents_ ```bash cat <<EOF This is Here Document. EOF ```
## Other Tips
* add `||` to commands expected to fail ```bash ls /root || { echo "Error" >&2 exit 1 } ``` note: * https://linuxhint.com/bash_error_handling/
## Questions?
![qr code](https://api.qrserver.com/v1/create-qr-code/?data=https://bit.ly/31skY47&size=300x300) (**https://bit.ly/31skY47**)