Shell Scripting
Shebang and Execution
#!/usr/bin/env bash
set -euo pipefail # exit on error, unset vars fail, pipe failures propagate
chmod +x deploy.sh
./deploy.sh
bash deploy.sh # explicit interpreter
source deploy.sh # run in current shell (rare for deploy scripts)
set -euo pipefail (the strict mode trifecta) catches most silent failures. Add set -x temporarily for debugging.
Variables and Arguments
name="gazehub"
readonly VERSION=1.0
declare -i count=0
echo "Hello, $name"
echo "Arg1: ${1:-default}" # default if unset or empty
echo "Arg1: ${1:?missing arg}" # exit if unset
# Special parameters
echo "Args count: $#"
echo "All args: $*"
echo "All args (quoted): $@"
echo "Exit code: $?"
echo "Script name: $0"
echo "PID: $$"
Arrays:
servers=(web1 web2 web3)
echo "${servers[1]}"
echo "${#servers[@]}"
for s in "${servers[@]}"; do echo "$s"; done
Conditionals
if [[ -f /etc/os-release ]]; then
source /etc/os-release
echo "Running $NAME $VERSION_ID"
elif [[ -d /tmp ]]; then
echo "tmp exists"
else
echo "fallback"
fi
# File tests
[[ -e path ]] # exists
[[ -f path ]] # regular file
[[ -d path ]] # directory
[[ -r path ]] # readable
[[ -x path ]] # executable
[[ -z "$var" ]] # empty string
[[ -n "$var" ]] # non-empty
[[ "$a" == "$b" ]]
[[ $count -gt 10 ]]
# Command success
if command -v curl &>/dev/null; then
curl -fsS https://example.com
fi
Use [[ ]] in Bash — safer than POSIX [ ] for quoting and pattern matching.
Loops
for file in /var/log/*.log; do
[[ -f "$file" ]] || continue
echo "Processing $file"
gzip "$file"
done
for i in {1..5}; do echo "Attempt $i"; done
while read -r line; do
echo "Line: $line"
done < input.txt
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=1; shift ;;
-f|--file) FILE="$2"; shift 2 ;;
*) echo "Unknown: $1"; exit 1 ;;
esac
done
Functions
log() {
echo "[$(date +%Y-%m-%dT%H:%M:%S)] $*" >&2
}
backup_dir() {
local src=$1 dest=$2
tar -czf "$dest" "$src"
}
# Return exit code
is_port_open() {
local port=$1
ss -tln | grep -q ":${port} "
}
log "Starting backup"
backup_dir /etc/nginx "/tmp/nginx-$(date +%F).tar.gz"
Use local for function variables to avoid polluting global scope.
Error Handling
set -e
trap 'echo "Failed at line $LINENO" >&2' ERR
trap cleanup EXIT
cleanup() {
rm -f /tmp/deploy.lock
}
if ! command -v curl &>/dev/null; then
echo "curl required" >&2
exit 1
fi
rm -f /tmp/stale.lock || true # ignore acceptable failures
Meaningful exit codes:
exit 0 # success
exit 1 # general error
exit 2 # misuse (wrong args)
Callers and CI systems rely on exit codes — never exit 0 on failure.
Here Documents and Strings
cat <<EOF > /tmp/config.yml
host: ${HOSTNAME}
env: production
EOF
# Literal (no expansion)
cat <<'EOF'
$VAR not expanded
EOF
Best Practices
| Practice | Reason |
|---|---|
Quote variables "$var" |
Prevents word splitting and globbing |
Use [[ ]] over [ ] |
Bash-native, handles empty strings safely |
Prefer $(cmd) over backticks |
Readable nesting |
Log to stderr with >&2 |
Keep stdout clean for pipes and capture |
Use shellcheck deploy.sh |
Catches common bugs before deploy |
readonly for constants |
Prevents accidental overwrite |
Avoid eval |
Injection risk |
# shellcheck directives when intentional
# shellcheck disable=SC2086
Common Mistakes
| Mistake | Consequence |
|---|---|
Unquoted $@ in loops |
Breaks args with spaces |
Missing pipefail |
Pipeline fails silently on middle command error |
cd without check |
Script runs in wrong directory after failed cd |
Parsing ls output |
Breaks on filenames with spaces/newlines |
# Safe cd
cd /opt/myapp || { echo "cd failed"; exit 1; }
Production-Ready Template
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/myapp/deploy.log"
log() { echo "[$(date +%T)] $*" | tee -a "$LOG_FILE" >&2; }
die() { log "ERROR: $*"; exit 1; }
main() {
[[ $# -ge 1 ]] || die "Usage: $0 <environment>"
local env=$1
log "Deploying to $env"
# deployment steps...
log "Done"
}
main "$@"
Troubleshooting
Run with trace: bash -x script.sh 2>&1 | tee trace.log
Test strict mode sections incrementally — legacy scripts may need set +e around known-flaky commands.
Production Scenario
A deploy script rolls out a new app version:
#!/usr/bin/env bash
set -euo pipefail
VERSION="${1:?version required}"
HEALTH_URL="http://localhost:8080/health"
deploy() {
log "Pulling version $VERSION"
docker pull "myapp:${VERSION}"
docker stop myapp || true
docker run -d --name myapp "myapp:${VERSION}"
for i in {1..30}; do
curl -fsS "$HEALTH_URL" && return 0
sleep 2
done
die "Health check failed"
}
rollback() {
log "Rolling back"
docker stop myapp || true
docker run -d --name myapp "myapp:previous"
}
trap 'rollback' ERR
deploy
trap - ERR
log "Deploy successful"
Failed health check triggers automatic rollback — no manual intervention at 3 AM.
Start small: wrap repeated commands, add set -euo pipefail, run shellcheck, then grow into reusable functions and proper CLI argument parsing.