Fundamentals 15 min read

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.

Programmer DD
Programmer DD
Programmer DD
Create Safer Bash Scripts with a Minimal, Portable Template

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_file

If 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

best practicesShell scriptingscript template
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.