Human-Friendly Design
- Provide
--helpand-hflags; show usage on invalid input - Use full words for long flags (
--outputnot--outp) - Confirm destructive actions unless
--forceis passed - Show progress for long operations; support
--quietand--verbose
Composability
- Write output to stdout by default but support
--output <file> - Write errors and diagnostics to stderr only
- Accept stdin when
-is given as filename - Produce one record per line for pipeline friendly text output
- Offer
--jsonfor machine-parseable output when appropriate
Exit Codes
0= success1= general error2= invalid usage
Error Messages
- Format:
scriptname: error: what went wrong - Include what happened and how to fix it
- Suggest
--helpon invalid usage
Debugging
- Add support for debuggin output if appropriate when a environment variable
DEBUG=1is set.
Robustness
- Validate arguments before doing any work
- Fail fast—don't partially complete then error
- Clean up temp files on exit (use trap)
Usage Help
Put CLI help as comments at the top of the script following this template:
#!/usr/bin/env bash
#
# What is the script doing?
#
# USAGE
#
# COMMAND SUB_SOMMAND --OPTION # help text for this command
#
# COMMANDS
#
# SUB_COMMAND_1 # COMMAND_1 does this
# SUB_COMMAND_2 # COMMAND_2 does that
#
# OPTIONS
#
# -a OPRION_A # OPTION_A does this
# -b OPRION_B # OPTION_B does that
#
# EXAMPLES
#
# COMMAND -a OPTION # This does this
#
set -euo pipefail
show_help() {
# shellcheck disable=SC2086
awk '/^[^ #]/{c=1}c==0{print $0}' $0 | sed -n '/^#/p' | sed 1d | sed 's/^#/ /g' |
perl -pe "s/ #(.*)$/$(tput setaf 0)\1$(tput sgr 0)/" |
perl -pe "s/(USAGE|EXAMPLES|COMMANDS|OPTIONS)/$(tput setaf 0)\1$(tput sgr 0)/" |
perl -pe "s/\`(.+)\`/$(tput sgr 0 1)\1$(tput sgr 0)/"
exit 1
}
show_help
Be idempotent where possible (safe to run twice).
On commands which are changing data or critical, ask for confirmation from the command user using this script
yesno() {
echo ""
read -r -p " $1 [Y/n] " response
[[ $response == "n" || $response == "N" ]] && exit 1
}
yesno "are you sure to do this?"
Script Header
Always start with:
#!/usr/bin/env bash
set -euo pipefail
Variables
- Lowercase for local:
local filename - UPPERCASE for exported/constants:
readonly VERSION="1.0.0" - Always quote:
"$var"not$var - Defaults:
${var:-default}, required:${var:?error message}
Conditionals
- Use
[[ ]]not[ ] - Use
(( ))for arithmetic - Check command existence:
command -v git &>/dev/null
Functions
- Use
localfor all variables - Keep functions short and single-purpose
- Return status with
return, output withecho
Color Codes
Use ANSI color codes; auto-disabled when stdout isn't a TTY (e.g. piped to a logfile, run in CI)
BOLD=$'\e[1m'; BLUE=$'\e[34m'; GREEN=$'\e[32m'; RED=$'\e[31m'; DIM=$'\e[2m'; RESET=$'\e[0m'
[[ -t 1 ]] || { BOLD=; BLUE=; GREEN=; RED=; DIM=; RESET=; }
Error Handling & Cleanup
info() { printf '%s●%s %s%s%s\n' "$BLUE" "$RESET" "$BOLD" "$*" "$RESET"; }
ok() { printf '%s✓%s %s\n' "$GREEN" "$RESET" "$*"; }
err() { printf '%s✗%s %s\n' "$RED" "$RESET" "$*" >&2; }
# `trap - ERR` disarms the ERR trap below so the explicit `exit 1` doesn't double-report.
die() { err "$*"; trap - ERR; exit 1; }
trap 'err "script failed at line $LINENO"' ERR
Argument Parsing Pattern
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-o|--output) output="$2"; shift 2 ;;
--) shift; break ;;
-*) die "unknown option: $1" ;;
*) break ;;
esac
done
Safe Iteration
# Over lines (handles whitespace)
while IFS= read -r line; do ...; done < "$file"
# Over globs (handles missing matches)
for f in *.txt; do [[ -e "$f" ]] || continue; ...; done
Always run shellcheck cli for static code analysis and lint tool
Usage
shellcheck -s bash script.sh
Accept disabled shellcheck rules
Add at top of file to disable rules in a file:
#!/usr/bin/env bash
# shellcheck disable=SC2003,SC2219
add at a specific line to disable line:
hexToAscii() {
# shellcheck disable=SC2059
printf "\x$1"
}
Bash Completion Scripts
Bash completion scripts resides in a subfolder bash_completion.d within the project. A Makefile installs the bash completion scripts alongside the script
itself into $(HOME)/etc/bash_completion.d.
Simple example:
#!/usr/bin/env bash
_name-of-script_completions() {
local cur prev
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=($(compgen -W "-h -a -b -c" -- ${cur}))
}
complete -F _name-of-script_completions name-of-script