Best Practices
The difference between a script that works and a script you can trust in production.
Start Every Script Right
#!/bin/bash
set -euo pipefail
This catches most common errors automatically.
Use ShellCheck
ShellCheck finds bugs before they bite:
Install it:
Run ShellCheck Always
Make it part of your workflow. Many editors have ShellCheck plugins. Zero warnings should be your goal.
Quote Everything
# Bad
cp $file $dest
rm $path
# Good
cp "$file" "$dest"
rm "$path"
Unquoted variables break on spaces and special characters.
Use Meaningful Names
# Bad
f=$1
d=/tmp
# Good
input_file="$1"
temp_dir="/tmp"
Future you will thank present you.
Use readonly and local
#!/bin/bash
# Constants
readonly CONFIG_FILE="/etc/app.conf"
readonly MAX_RETRIES=3
process_file() {
local input="$1"
local output="${2:-/dev/stdout}"
# $input and $output only exist in this function
}
Fail Fast with Clear Messages
#!/bin/bash
set -euo pipefail
die() {
echo "ERROR: $1" >&2
exit 1
}
# Check requirements upfront
[[ -f "$config" ]] || die "Config not found: $config"
[[ -d "$output_dir" ]] || die "Output directory missing: $output_dir"
command -v jq &>/dev/null || die "jq is required but not installed"
Use Functions
#!/bin/bash
# Bad: 200 lines of sequential code
# Good: Organized into functions
parse_arguments() { ... }
validate_input() { ... }
process_data() { ... }
generate_report() { ... }
main() {
parse_arguments "$@"
validate_input
process_data
generate_report
}
main "$@"
Handle Cleanup
#!/bin/bash
set -euo pipefail
temp_file=""
cleanup() {
[[ -n "$temp_file" ]] && rm -f "$temp_file"
}
trap cleanup EXIT
temp_file=$(mktemp)
# Script continues... cleanup runs no matter how it exits
Use mktemp for Temp Files
# Bad - predictable, insecure
temp="/tmp/mydata.txt"
# Good - random, safe
temp=$(mktemp)
temp_dir=$(mktemp -d)
Avoid Common Pitfalls
Don't Parse ls
# Bad
for f in $(ls *.txt); do ...
# Good
for f in *.txt; do
[[ -f "$f" ]] || continue
...
done
Don't Use cat Unnecessarily
# Bad
cat file.txt | grep pattern
# Good
grep pattern file.txt
Use [[ ]] Not [ ]
# Bad - word splitting issues
if [ $var = "value" ]; then
# Good - safer
if [[ "$var" == "value" ]]; then
Script Template
#!/bin/bash
#
# script-name.sh - Brief description
#
# Usage: script-name.sh [options] <arguments>
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly SCRIPT_NAME="${0##*/}"
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [options] <input-file>
Options:
-o, --output FILE Output file (default: stdout)
-v, --verbose Enable verbose output
-h, --help Show this help
Example:
$SCRIPT_NAME -o result.txt input.txt
EOF
exit 0
}
die() {
echo "ERROR: $1" >&2
exit 1
}
log() {
[[ "${VERBOSE:-false}" == "true" ]] && echo "$@"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) OUTPUT="$2"; shift 2 ;;
-v|--verbose) VERBOSE=true; shift ;;
-h|--help) usage ;;
-*) die "Unknown option: $1" ;;
*) INPUT="$1"; shift ;;
esac
done
[[ -n "${INPUT:-}" ]] || die "Input file required"
}
main() {
parse_args "$@"
log "Processing: $INPUT"
# ... main logic ...
log "Done"
}
main "$@"
Why should you quote variables like $var?
Checklist
Before deploying a script:
-
set -euo pipefailat top - ShellCheck passes with no warnings
- All variables quoted
- Functions for organization
- Cleanup with trap
- Clear error messages
- Usage/help text
- Tested error paths
Key Takeaways
- Always use
set -euo pipefail - Run ShellCheck on every script
- Quote all variables:
"$var" - Use
localin functions,readonlyfor constants - Handle cleanup with
trap ... EXIT - Use
mktempfor temp files - Test failure cases, not just success
Congratulations! You've completed Chapter 13: Advanced Scripting.
Next chapter: Sysadmin Essentials - managing users, services, and cron jobs.