Create Safer Bash Scripts with a Minimal, Portable Template
This guide introduces a minimal, safe Bash script template, explains why Bash is still essential for backend tasks, and walks through best‑practice features such as strict error handling, portable shebang, colorized messaging, parameter parsing, cleanup traps, and portability across macOS and various Linux distributions.
Why write Bash scripts
Although Bash is not a mainstream language, it is ubiquitous on every Linux system and is the default shell for many backend environments, making it indispensable for tasks such as starting server applications, CI/CD steps, or integration‑test scripts.
The opposite of "it's like riding a bike" is "it's like programming in bash". A phrase which means that no matter how many times you do something, you will have to re‑learn it every single time. — Jake Wharton (@JakeWharton) December 2, 2020
Bash inherits the legacy of the original shell and is available on virtually every Linux machine, which is why it is often used for scripting on production servers, Docker images, or CI environments.
Bash script template
Below is a minimal, safe Bash script template that you can copy and adapt.
#!/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"
}
parse_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 ;;
-p|--param) param="$2"; shift ;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
[[ -z "${param-}" ]] && die "Missing required parameter: param"
[[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
return 0
}
parse_params "$@"
setup_colors
msg "${RED}Read parameters:${NOFORMAT}"
msg "- flag: ${flag}"
msg "- param: ${param}"
msg "- arguments: ${args[*]-}"Choose Bash
The script uses /usr/bin/env for maximum compatibility instead of hard‑coding /bin/bash.
Fail fast
The set -Eeuo pipefail command makes the script exit immediately on any error, preventing situations where a later command runs after a previous failure (e.g., deleting a file after a failed backup).
#!/usr/bin/env bash
cp important_file ./backups/
rm important_fileIf the backup directory does not exist, the script will abort before the file is removed.
Get the location
This line determines the directory where the script resides, allowing the script to reference files relative to its own location regardless of the current working directory.
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)When the script is invoked from a CI tool with an absolute path, using script_dir ensures the script still operates on files relative to its own directory.
Try to clean up
trap cleanup SIGINT SIGTERM ERR EXIT
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
# script cleanup here
}The cleanup function runs automatically when the script exits, allowing you to remove temporary files or perform other teardown tasks.
Display helpful help
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]
Script description here.
... (more description)
EOF
exit
}Placing the usage function near the top of the script provides quick reference for users and serves as minimal documentation.
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"
}The msg function writes messages to stderr, keeping them separate from the script's actual output. This follows the "stdout for output, stderr for messaging" principle.
Parse any parameters
parse_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 ;;
-p|--param) param="$2"; shift ;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
[[ -z "${param-}" ]] && die "Missing required parameter: param"
[[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
return 0
}This manual while / case loop parses flags, named parameters, and positional arguments. It demonstrates how to handle unknown options and enforce required parameters.
Using the template
To adapt the template, replace the usage text, customize the cleanup function, adjust the example flags/parameters in parse_params, and insert your actual script logic where indicated.
Portability
The template has been tested on macOS (bash 3.2) and several Docker images (Debian, Ubuntu, CentOS, Amazon Linux, Fedora). It works on any system that provides Bash, but not on minimal images that lack Bash (e.g., Alpine without bash).
Further reading
Command Line Interface Guidelines (https://clig.dev/)
12 Factor CLI Apps (https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46)
Command line arguments anatomy explained with examples (https://betterdev.blog/command-line-arguments-anatomy-explained/)
Closing notes
This is not the first Bash script template, but it provides a solid, portable foundation that can be trimmed down for simple tasks while still supporting robust error handling and clean‑up.
When writing Bash scripts, use an IDE that supports the ShellCheck linter (e.g., JetBrains IDEs) to catch common pitfalls early.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
