Structuring Larger Scripts and Tools
Prerequisites: Parsing options with getopts, Testing scripts with Bats
A 20-line script can be a flat list of commands. A tool used by a team needs structure: clear entry points, reusable functions, help text, and tests.
A template for a real tool
cat > tool.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
readonly VERSION="1.2.0"
readonly SCRIPT_NAME=${0##*/}
usage() {
cat <<USAGE
Usage: $SCRIPT_NAME <command> [options]
Commands:
greet <name> Print a greeting
version Show the version
Options:
-h, --help Show this help
USAGE
}
die() { echo "$SCRIPT_NAME: $*" >&2; exit 1; }
cmd_greet() {
[[ $# -ge 1 ]] || die "greet: missing <name>"
echo "Hello, $1!"
}
cmd_version() { echo "$SCRIPT_NAME $VERSION"; }
main() {
local command=${1:-}
[[ -n $command ]] || { usage; exit 2; }
shift
case "$command" in
greet) cmd_greet "$@" ;;
version) cmd_version ;;
-h|--help) usage ;;
*) die "unknown command: $command (try --help)" ;;
esac
}
main "$@"
EOF
bash tool.sh greet Ada
bash tool.sh version
bash tool.sh greet || echo "exit code: $?"
bash tool.sh launch || echo "exit code: $?"
Hello, Ada!
tool.sh 1.2.0
tool.sh: greet: missing <name>
exit code: 1
tool.sh: unknown command: launch (try --help)
exit code: 1
What makes it maintainable:
- Strict mode and a
diehelper for consistent errors. readonlyconstants for settings that must not change.- One function per subcommand (
cmd_greet,cmd_version), dispatched frommainwithcase— the same pattern asgitanddocker. - A usage message built from a here document.
main "$@"at the bottom, so all functions are defined before anything runs.
Splitting into files
Shared helpers can live in a library file that scripts source:
# illustrative
# lib/common.sh
log() { printf '%s %s\n' "$(date +%T)" "$*" >&2; }
require_cmd() { command -v "$1" > /dev/null || die "missing dependency: $1"; }
# illustrative
# bin/deploy
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "$script_dir/../lib/common.sh"
require_cmd rsync
Finding the library relative to the script (BASH_SOURCE) makes the tool
work wherever it's installed. command -v checks for required programs
up front, with a clear error.
Configuration
Read settings in a clear order of precedence: built-in defaults, then a
config file (~/.config/tool/config), then environment variables, then
command-line options.
Keep the tool healthy
- ShellCheck and Bats in CI.
- A README with installation, examples, and every option.
- A
--versionflag and a changelog.