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.