Source From Here
PrefaceBash scripts. Almost anyone needs to write one sooner or later. Almost no one says “yeah, I love writing them”. And that’s why almost everyone is putting low attention while writing them. I won’t try to make you a Bash expert (since I’m not a one either), but I will show you a minimal template that will make your scripts safer. You don’t need to thank me, your future self will thank you.
Why scripting in Bash?
But Bash has something in common with another widely beloved language. Just like JavaScript, it won’t go away easily. While we can hope that Bash won’t become the main language for literally everything, it’s always somewhere near.
Bash inherited the shell throne and can be found on almost every Linux, including Docker images. And this is the environment in which most of the backend runs. So if you need to script the server application startup, a CI/CD step, or integration test run, Bash is there for you.
To glue few commands together, pass output from one to another, and just start some executable, Bash is the easiest and most native solution. While it makes perfect sense to write bigger, more complicated scripts in other languages, you can’t expect to have Python, Ruby, fish, or whatever another interpreter you believe is the best, available everywhere. And you probably should think twice and then once more before adding it to some prod server, Docker image, or CI environment.
Yet Bash is far from perfect. The syntax is a nightmare. Error handling is difficult. There are landmines everywhere. And we have to deal with it.
Bash script template
Without further ado, here it is.
- #!/usr/bin/env bash
- set -Eeuo pipefail
- trap cleanup SIGINT SIGTERM ERR EXIT
- script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
- usage() {
- cat <<EOF
- Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]
- Script description here.
- Available options:
- -h, --help Print this help and exit
- -v, --verbose Print script debug info
- -f, --flag Some flag description
- -p, --param Some param description
- EOF
- exit
- }
- cleanup() {
- trap - SIGINT SIGTERM ERR EXIT
- # script cleanup here
- }
- setup_colors() {
- if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
- NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
- else
- NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
- fi
- }
- msg() {
- echo >&2 -e "${1-}"
- }
- die() {
- local msg=$1
- local code=${2-1} # default exit status 1
- msg "$msg"
- exit "$code"
- }
- parse_params() {
- # default values of variables set from params
- flag=0
- param=''
- while :; do
- case "${1-}" in
- -h | --help) usage ;;
- -v | --verbose) set -x ;;
- --no-color) NO_COLOR=1 ;;
- -f | --flag) flag=1 ;; # example flag
- -p | --param) # example named parameter
- param="${2-}"
- shift
- ;;
- -?*) die "Unknown option: $1" ;;
- *) break ;;
- esac
- shift
- done
- args=("$@")
- # check required params and arguments
- [[ -z "${param-}" ]] && die "Missing required parameter: param"
- [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
- return 0
- }
- parse_params "$@"
- setup_colors
- # script logic here
- msg "${RED}Read parameters:${NOFORMAT}"
- msg "- flag: ${flag}"
- msg "- param: ${param}"
- msg "- arguments: ${args[*]-}"
One solution would be to have a separate script with all the boilerplate and utility functions and execute it at the beginning. The downside would be to have to always attach this second file everywhere, losing the “simple Bash script” idea along the way. So I decided to put in the template only what I consider to be a minimum to keep it possible short.
Fail fast
- set -Eeuo pipefail
- #!/usr/bin/env bash
- cp important_file ./backups/
- rm important_file
Get the location
- script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
But if, let’s say, our CI config executes script like this:
then our script is operating not in project dir, but some completely different workdir of our CI tool. We can fix it, by going to the directory before executing the script:
- cd /opt/ci/project && ./script.sh
- cat "$script_dir/my_file"
Try to clean up
- trap cleanup SIGINT SIGTERM ERR EXIT
- cleanup() {
- trap - SIGINT SIGTERM ERR EXIT
- # script cleanup here
- }
Just remember that the cleanup() can be called not only at the end but as well having the script done any part of the work. Not necessarily all the resources you try to cleanup will exist.
Display helpful help
- usage() {
- cat <<EOF
- Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]
- Script description here.
- ...
- EOF
- exit
- }
I don’t argue to document every function here. But a short, nice script usage message is a required minimum.
Print nice messages
- setup_colors() {
- if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
- NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
- else
- NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
- fi
- }
- msg() {
- echo >&2 -e "${1-}"
- }
Messages printed with msg() are sent to stderr stream and support special sequences, like colors. And colors are disabled anyway if the stderr output is not an interactive terminal or one of the standard parameters is passed. Usage:
- msg "This is a ${RED}very important${NOFORMAT} message, but not a script output value!"
Parse any parameters
- parse_params() {
- # default values of variables set from params
- flag=0
- param=''
- while :; do
- case "${1-}" in
- -h | --help) usage ;;
- -v | --verbose) set -x ;;
- --no-color) NO_COLOR=1 ;;
- -f | --flag) flag=1 ;; # example flag
- -p | --param) # example named parameter
- param="${2-}"
- shift
- ;;
- -?*) die "Unknown option: $1" ;;
- *) break ;;
- esac
- shift
- done
- args=("$@")
- # check required params and arguments
- [[ -z "${param-}" ]] && die "Missing required parameter: param"
- [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
- return 0
- }
There are three main types of CLI parameters – flags, named parameters, and positional arguments. The parse_params() function supports them all. The only one of the common parameter patterns, that is not handled here, is concatenated multiple single-letter flags. To be able to pass two flags as -ab, instead of -a -b, some additional code would be needed.
The while loop is a manual way of parsing parameters. In every other language you should use one of the built-in parsers or available libraries, but, well, this is Bash.
An example flag (-f) and named parameter (-p) are in the template. Just change or copy them to add other params. And do not forget to update the usage() afterward.
The important thing here, usually missing when you just take the first google result for Bash arguments parsing, is throwing an error on an unknown option. The fact the script received an unknown option means the user wanted it to do something that the script is unable to fulfill. So user expectations and script behavior may be quite different. It’s better to prevent execution altogether before something bad will happen.
There are two alternatives for parsing parameters in Bash. It’s getopt and getopts. There are arguments both for and against using them. I found those tools not best, since by default getopt on macOS is behaving completely differently, and getopts does not support long parameters (like --help).
Using the template
Just copy-paste it, like most of the code you find on the internet. Well, actually, it was quite honest advice. With Bash, there is no universal npm install equivalent. After you copy it, you only need to change 4 things:
Portability
I tested the template on MacOS (with default, archaic Bash 3.2) and several Docker images: Debian, Ubuntu, CentOS, Amazon Linux, Fedora. It works.
Obviously, it won’t work on environments missing Bash, like Alpine Linux. Alpine, as a minimalistic system, uses very lightweight ash (Almquist shell).
You could ask a question if it wouldn’t be better to use Bourne shell-compatible script that will work almost everywhere. The answer, at least in my case, is no. Bash is safer and more powerful (yet still not easy to use), so I can accept the lack of support for a few Linux distros that I rarely have to deal with.
沒有留言:
張貼留言