A production-ready Bash scripting template with best practices, robust error handling, and useful utilities built-in.
Licensed under BSD 3-Clause License.
- Zero dependencies: All code is original and thoroughly audited by the author.
- Options-only design: No positional arguments or subcommands—options are the single source of truth, prioritizing simplicity.
- Structured option handling: Options are defined with clear metadata, allowing reliable registration, parsing, and validation.
- Multi-level, color-coded logging: Output messages at five levels (Debug, Info, Warning, Error, Critical) with color support.
- Reliable locking mechanism: Prevents concurrent script execution.
- Graceful termination handling: Comprehensive trap handlers for
EXITandERR. - Quiet mode: Supports silent operation.
- Flexible modes: Choose from four modes (lite, full, legacy, source/script) to suit different use cases.
Clone the repository:
git clone --depth=1 https://github.com/Silverbullet069/bash-script-template.git
cd bash-script-templateDownloaded script files aren't executable by default. Change scripts permissions:
chmod +x *.sh && chmod -x source.sh(optional) Create a symlink out of clone.sh:
Important
Make sure ~/.local/bin is inside your PATH environment variable.
ln -s "$(PWD)/clone.sh"~/.local/bin/cloneCreate your first template:
clone
clone -m full -o path/to/script.bash
# read more: clone -hTip
Use .bash extension if preferred - although both .sh and .bash are supported.
| File | Purpose |
|---|---|
template.sh | Self-contained script combining all functionality |
template_lite.sh | A small, simple yet reliable template script |
template_legacy.sh | template.sh version 2.4.0 |
source.sh | Reusable library functions |
script.sh | Main script template |
build.sh | Merges source.sh + script.sh → template.sh |
clone.sh | Helper to clone template with placeholder replacement |
Register new options by writing into options_init():
functionoption_init() {
# NOTE: long-name, short-name, default, help, type, required, constraints# register_option ...# CAUTION: --help must be placed as the first option in the built-in options list# CAUTION: I add a blank link on top of this function inside help message
register_option "--help""-h"false"Display this help and exit""bool"
register_option "--log-level""-l""INF""Specify log level""choice"false"DBG,INF,WRN,ERR"
register_option "--timestamp""-t"false"Enable timestamp output""bool"
register_option "--no-color""-n"false"Disable color output""bool"
register_option "--quiet""-q"false"Run silently unless an error is encountered""bool"
}The register_option() functions takes 7 parameters: long-name, short-name, default, help, type, required, constraints.
After that, write into main() function:
functionmain() {
trap script_trap_err ERR
trap script_trap_exit EXIT
script_init "$@"
option_init
parse_params "$@"
quiet_init
color_init
lock_init user
# start here# ...
}Write directly into main() function:
functionmain() {
trap script_trap_exit EXIT
lock_init "user"# start here# ...
}Add options to the parse_params() function using this pattern:
functionparse_params() {
# ...case"${param}"in
-m | --mock)
### Description for mock option. @DEFAULT:default_value@# Add validation logic here# ...# variable naming convention: _option_<option-name>
_option_mock="${1-}"shift
;;
# ...esac
}Parsing rules:
- Lines starting with
###become help text displayed when specified-h|--helpoption - Use
@DEFAULT:value@to set default values. It's automatically removed from help text. - For boolean flags, omit the value assignment and shift.
Example script:
#!/usr/bin/env bashsource source.sh
functionparse_params() {
# Add your custom options herecase"${param}"in
-f | --file)
### Input file path. @DEFAULT:input.txt@
_option_file="${1}"shift
;;
# ...built-in options...esac
}
functionmain() {
script_init "$@"
parse_params "$@"
info "Processing file: ${_option_file}"# Your logic here
}
main "$@"Options-only approach: This template is designed to handle options exclusively—no subcommands or positional arguments. For complex multi-command CLIs, use mature frameworks like Click (Python), Cobra (Go), or similar tools that are battle-tested for subcommand handling.
Conservative default syntax (e.g.
${param:-}): Use only for optional arguments and explicit exceptions. AVOID overusing on every variable.Fail-fast philosophy:
- Enable
set -eto terminate the script when undefined variables are referenced unexpectedly. - Use
script_exit()to halt execution on critical logic failures. - Avoid using log ERR messages and return non-zero exit codes to replace
script_exit()
- Enable
Function argument validation: Implement conditional checks
[[ -z ... ]]withscript_exit(), then assign positional parameters (e.g.,$1, $2) to descriptive local variables (e.g.$param_name).Logging strategy:
- Log in both reusable functions and calling functions.
- Calling functions may suppress reusable function output via pipe redirection.
- Success messages in reusable functions are optional (DBG level).
- Success messages in calling functions are mandatory (INF level).
Logging message convention:
<function name> (optional): N + <V-ed> + O: <variable>. AVOID modifying this format as it may break existing test cases.Omit redundant
return 0: Unnecessary at function end.Enable
set -e: Forces immediate exit on non-zero exit codes. Despite complexity and edge cases with expected failures, this template enables it because:- Scripts written for
errexitcompatibility work without it, but not vice versa - Production benefits outweigh drawbacks
- Can be disabled when necessary without breaking functionality
- Scripts written for
Enable
set -u: Exits when expanding unset variables, catching typos and premature variable access. Enabled following the same rationale aserrexit- better to maintain compatibility while allowing selective disabling.