Skip to content

Instantly share code, notes, and snippets.

@M1lan
Last active July 30, 2025 21:17
Show Gist options
  • Save M1lan/1454743f94f455a0aeff692c29a71ea0 to your computer and use it in GitHub Desktop.
Save M1lan/1454743f94f455a0aeff692c29a71ea0 to your computer and use it in GitHub Desktop.
Bash template
#!/usr/bin/env bash
##
# Note: almost always, we /want/ to inherit the encapsulating shell
# environment. If we don't want or "trust" that environment, we can
# run scripts from within a clean shell environment like this:
# #!/usr/bin/env -iS -- bash
#
# If anyone knows about an actual use case for this, pls let me know.
##
# Shellcheck configuration example for your .shellcheckrc:
#
#
# enable=add-default-case
# enable=avoid-nullary-conditions
# enable=check-extra-masked-returns
# enable=check-set-e-suppressed
# enable=check-unassigned-uppercase
# enable=deprecate-which
# enable=quote-safe-variables
# enable=require-double-brackets
# enable=require-variable-braces
# external-sources=true
##
# In production scenarios, we want to know about all failure
# conditions and ensure they are handled correctly. Bash is not the
# best language for that and we need to know the subtle differences
# between exit codes and what is printed to stdout/stderr and what
# means sucess or failure in the specific case we execute scripted
# commands or external commands. Using `set -euo pipefail` doesn't
# guarantee anything. The idea is more that instead of keep going at
# all costs, we want to fail early.
#
# If in doubt, use Python or Go.
# If multiple kilo-SLOC script, use Python or Go.
#
# Instead of a library of scripts, write one cli with Go cobra/kong.
#
# If you want it to be a "TUI" kind of interactive terminal tool,
# consider Rust. Or at least Go.
#
# "Interactive" is irrelevant for production. apply YAGNI. What's
# usually important for production is to fail early and give a good
# error message, the return code is usually less important. We
# normally treat scripts as if they are normal programs that just
# suceed or fail, with Bash we have the luxury to do more but we
# rarely want/need it because that assumes we have knowledge about the
# system that we run on (which again, sysadmin experience is a whole
# different story from learning a scripting language also used in
# sysadmin applying it in a pure cloud environment. We can also just
# "keep going" by appending || true which is usually bad style unless
# we don't care about how a subshell exits.
#
# In any case, we need to not only know the scripting language, but
# also understand what will happen if the script runs on a certain
# system, and for that we need to know that system!
#
# Make sure to check version of Bash and understand the scripting
# language capacities (aka bashisms) of that version. Make extra sure
# to check the versions of the command line tools you use inside of
# our scripts.
#
# Make sure to check if you want/need POSIX sh in which case you
# cannot use many of Bash's convenient language features.
#
# You don't need strict POSIX compliance for any modern scenarios, and
# Linux itself is not strictly POSIX compliant.
#
# "sh" is actually still bash on most systems; only very rarely will
# you still encounter a bourne shell or a korn shell.
#
# zsh won't be available anywhere else but on your MacBook bash will
# always be available (both can be installed/updated).
#
# Bash (and Python, Go) is consent for scripting on Unix/Linux, while
# many people prefer zsh or fish for interactive use.
#
# Your script can be portable by installing bash, or it can be
# portable by being "posixly correct" which is highly irrelevant
# unless you are working on older mainframes,- that certainly won't
# ever happen in the cloud.
#
# Always use `shellcheck`.
#
# Column limit: 79 columns (similar to PEP-8).
#
# Semi-reliable exit codes: If anything in the script exits with a
# non-zero exit code, we want the whole script to stop immediately
# and also return with the same code. Never use `<command> || true`
# unless we really don't care about <command>. C.f. BASHFAQ 105.
# Use leading underscores on internal variable and function names
# in order to avoid name collisions. For unintentionally global
# variables defined without `local`, such as those defined outside of
# a function or automatically through a `for` loop, prefix with
# double underscores.
# You don't have to /always/ use braces when referencing variables,
# `"Hi, my name is: ${NAME}."` instead of `"Hi, my name is
# $NAME."`. While quotes are required for variable references, braces
# are only required in some cases.
# Bad: `some_command $arg1 $arg2 $arg3`
# Bad: `some_command ${arg1} ${arg2} ${arg3}`
# Bad: `echo "somestring $somevar somestring$someothervar"
# Good: `some_command "${arg1}" "${arg2}" "${arg3}"`
# Good: `some_command "$arg1" "$arg2" "$arg3"`
# Good: `echo "somestring${somevar} somestring${someothervar}"
#
# All 6 Good and Bad are valid, but the Good ones are readable.
# Everything else synax results in sytax errors.
#
# - Prefer `printf` over `echo`. For more information, see:
# http://unix.stackexchange.com/a/65819
#
# - Prefer `$_explicit_variable_name` over names like `$var`.
#
# - Underscores as function name prefix for internal functions
#
# - Use the `#!/usr/bin/env bash` shebang in order to run the
# preferred Bash version rather than hard-coding the executable
# path.
#
# For simple option parsing, getopts can be sufficient:
# while getopts "hdx:o:" opt; do ... done
# For complex cases with long options, use the manual parsing shown below.
#####################################################################
# header #
#####################################################################
# exit immediately on err; useful for CI/CD.
set -e
# exit if referencing undefined var.
set -u
# return exit code of failed command in pipe, not last.
set -o pipefail
# filenames might have spaces
IFS=$'\n\t'
# Version information
readonly _VERSION="1.0.0"
# Declare and assign separately to avoid masking return values
_BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
readonly _BUILD_DATE
# This script's basename.
_SELF="$(basename "${0}")"
readonly _SELF
# Save original directory and script directory
readonly _ORIGINAL_DIR="${PWD}"
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly _SCRIPT_DIR
# Temporary directory for this script
_TEMP_DIR=""
_PRINT_HELP=0
_USE_DEBUG=0
__DEBUG_COUNTER=0
_OPTION_X=0
_OPTION_VALUE=
# everything that comes after '--' is not an option flag, but usually
# the filename we run the script on or an action command.
_TARGET_FILE_OR_ACTION=
# Log levels for structured logging
readonly _LOG_LEVELS=(ERROR WARN INFO DEBUG)
_LOG_LEVEL="${LOG_LEVEL:-INFO}"
#####################################################################
# helpers #
#####################################################################
##
# _cleanup()
#
# Description: Cleanup function called on exit
_cleanup() {
local exit_code=$?
# Remove temp files if created
if [[ -n "${_TEMP_DIR}" ]] && [[ -d "${_TEMP_DIR}" ]]; then
rm -rf "${_TEMP_DIR}"
fi
exit "${exit_code}"
}
# Set traps for cleanup and interruption
trap _cleanup EXIT
trap '_exit_1 printf "Script interrupted\\n"' INT TERM
##
# _log()
#
# Usage:
# _log LEVEL "message"
#
# Description: Structured logging with levels
_log() {
local level="${1}"
shift
local level_num
case "${level}" in
ERROR) level_num=0 ;;
WARN) level_num=1 ;;
INFO) level_num=2 ;;
DEBUG) level_num=3 ;;
*) return 1 ;;
esac
# Compare with current log level
local current_level_num
case "${_LOG_LEVEL}" in
ERROR) current_level_num=0 ;;
WARN) current_level_num=1 ;;
INFO) current_level_num=2 ;;
DEBUG) current_level_num=3 ;;
*) current_level_num=2 ;; # Default to INFO if unknown
esac
if [[ ${level_num} -le ${current_level_num} ]]; then
local timestamp
timestamp="$(date -Iseconds)" || timestamp="[timestamp error]"
printf "[%s] %s: %s\\n" "${timestamp}" "${level}" "$*" >&2
fi
}
##
# _debug()
#
# Usage:
# _debug <command> <options>
#
# Description: Print debug info to stderr.
#
# Example: _debug printf "[DEBUG] Variable: %s\\n" "$0"
_debug() {
if ((${_USE_DEBUG:-0})); then
__DEBUG_COUNTER=$((__DEBUG_COUNTER + 1))
{
printf "――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n"
printf "🐛 %s " "${__DEBUG_COUNTER} >>"
"${@}"
printf "――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n"
} 1>&2
fi
}
##
# _exit_1()
#
# Usage:
# _exit_1 <command>
#
# Description: Exit with status 1 after sending a message to stdout
# and stderr.
_exit_1() {
{
if [[ -t 2 ]]; then # Check if stderr is a terminal
printf "%s " "$(tput setaf 1 || true)!$(tput sgr0 || true)"
else
printf "! "
fi
"${@}"
} 1>&2
exit 1
}
##
# _warn()
#
# Usage:
# _warn <command>
#
# Description: Print warning about a non-critical error to stdout and
# stderr, but don't exit.
_print_warning() {
{
if [[ -t 2 ]]; then # Check if stderr is a terminal
printf "%s " "$(tput setaf 1 || true)!$(tput sgr0 || true)"
else
printf "! "
fi
"${@}"
} 1>&2
}
##
# _require_commands()
#
# Usage:
# _require_commands command1 command2 ...
#
# Description: Verify required commands exist
_require_commands() {
local missing=()
for cmd in "$@"; do
if ! command -v "${cmd}" >/dev/null 2>&1; then
missing+=("${cmd}")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
_exit_1 printf "Missing required commands: %s\\n" "${missing[*]}"
fi
}
##
# _create_temp_dir()
#
# Usage:
# _create_temp_dir
#
# Description: Create a temporary directory for this script
_create_temp_dir() {
_TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/${_SELF}.XXXXXX")
readonly _TEMP_DIR
_debug printf "Created temp directory: %s\\n" "${_TEMP_DIR}"
}
##
# _print_help()
#
# Usage:
# _print_help
#
# Description: Print the program help information.
_print_help() {
cat <<EOF
${_SELF} version ${_VERSION} (built ${_BUILD_DATE})
Put your helpful text here.
Usage:
${_SELF} [--options] [<arguments>] [--] [target, action, or file]
${_SELF} -h | --help
Options:
-h, --help Display this help information.
-d, --debug Debug mode.
-x, --option-x A simple option 'x'.
-o, --option-with-value Some option, followed by a required value.
EOF
}
##
# __get_option_value()
#
# Usage:
# __get_option_value <option> <value>
#
# Description: For the given option, return the value or exit 1 if
# value is blank or appears to be another option.
__get_option_value() {
local __arg="${1:-}"
local __val="${2:-}"
if [[ -n "${__val:-}" ]] && [[ ! "${__val:-}" =~ ^- ]]; then
printf "%s\\n" "${__val}"
else
_exit_1 printf "%s requires a valid argument.\\n" "${__arg}"
fi
}
# parse options
while ((${#})); do
__arg="${1:-}"
__val="${2:-}"
case "${__arg}" in
-h | --help)
_PRINT_HELP=1
;;
-d | --debug)
_USE_DEBUG=1
;;
-x | --option-x)
_OPTION_X=1
;;
-o | --option-with-value)
_OPTION_VALUE="$(__get_option_value "${__arg}" "${__val:-}")"
shift
;;
--)
_TARGET_FILE_OR_ACTION="$(
IFS=$' '
printf "%s" "${*:2}"
)"
break
;;
-*)
_exit_1 printf "Unexpected option: %s\\n" "${__arg}"
;;
*)
_TARGET_FILE_OR_ACTION="$(
IFS=$' '
printf "%s" "${*:1}"
)"
break
;;
esac
shift
done
#####################################################################
# script starts here #
#####################################################################
##
# _example_function_returns()
#
# Description: Examples of proper function return values
_example_function_returns() {
# Return success/failure
_check_file_exists() {
if [[ -f "${1}" ]]; then
return 0 # success
else
return 1 # failure
fi
}
# Return values via stdout
_get_computed_value() {
local result="computed value"
printf "%s" "${result}"
}
# Usage examples
# Note: When used in 'if', set -e is disabled for the function
local file_check_result=0
# Temporarily disable to capture exit code
set +e
_check_file_exists "/etc/passwd"
file_check_result=$?
set -e
if [[ ${file_check_result} -eq 0 ]]; then
_debug printf "File exists\\n"
fi
local value
value=$(_get_computed_value)
_debug printf "Got value: %s\\n" "${value}"
}
##
# _example_array_handling()
#
# Description: Examples of proper array usage
_example_array_handling() {
# Good array practices
local -a my_array=()
my_array+=("element1")
my_array+=("element with spaces")
my_array+=("third element")
# Iterate safely
for element in "${my_array[@]}"; do
_debug printf "Element: %s\\n" "${element}"
done
# Pass arrays to functions (bash 4.3+)
_process_array() {
local -n arr=$1 # nameref
for item in "${arr[@]}"; do
_debug printf "Processing: %s\\n" "${item}"
done
}
_process_array my_array
}
_example() {
_debug printf "this is a debug message...\\n"
# Log examples
_log INFO "Script starting"
_log DEBUG "Debug information"
# Check for required commands (example)
# _require_commands jq curl git
## handle options
if ((_OPTION_X)); then
printf "Received -x. Doing nothing.\\n"
_debug printf "the --option-x flag is set.\\n"
else
_debug printf "the --option-x flag is NOT set.\\n"
fi
if [[ -n "${_OPTION_VALUE}" ]]; then
printf "Option value: %s\\n" "${_OPTION_VALUE}"
fi
if [[ -n "${_TARGET_FILE_OR_ACTION}" ]]; then
printf "Operating on file / selected action: %s\\n" "${_TARGET_FILE_OR_ACTION}"
fi
# Demonstrate array handling
_example_array_handling
# Demonstrate function returns
_example_function_returns
# Create temp directory if needed
# _create_temp_dir
# echo "temp file" > "${_TEMP_DIR}/example.txt"
## do stuff here!
printf "Bash is here: %s\\n" "$(command -v bash || true)"
printf "Script ran successfully.\\n"
}
# _main()
#
# Usage:
# _main [<options>] [<arguments>]
#
# Description:
# Entry point.
_main() {
if ((_PRINT_HELP)); then
_print_help
else
_example "$@"
fi
}
_main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment