Best Practices

The difference between a script that works and a script you can trust in production.

Start Every Script Right

hljs bash
#!/bin/bash
set -euo pipefail

This catches most common errors automatically.

Use ShellCheck

ShellCheck finds bugs before they bite:

Terminal
$shellcheck script.sh
In script.sh line 3: echo $name ^---^ SC2086: Double quote to prevent globbing

Install it:

Terminal
$sudo apt install shellcheck
$# Or use online: shellcheck.net

Run ShellCheck Always

Make it part of your workflow. Many editors have ShellCheck plugins. Zero warnings should be your goal.

Quote Everything

hljs bash
# Bad
cp $file $dest
rm $path

# Good
cp "$file" "$dest"
rm "$path"

Unquoted variables break on spaces and special characters.

Use Meaningful Names

hljs bash
# Bad
f=$1
d=/tmp

# Good
input_file="$1"
temp_dir="/tmp"

Future you will thank present you.

Use readonly and local

hljs bash
#!/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

hljs bash
#!/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

hljs bash
#!/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

hljs bash
#!/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

hljs bash
# Bad - predictable, insecure
temp="/tmp/mydata.txt"

# Good - random, safe
temp=$(mktemp)
temp_dir=$(mktemp -d)

Avoid Common Pitfalls

Don't Parse ls

hljs bash
# Bad
for f in $(ls *.txt); do ...

# Good
for f in *.txt; do
    [[ -f "$f" ]] || continue
    ...
done

Don't Use cat Unnecessarily

hljs bash
# Bad
cat file.txt | grep pattern

# Good
grep pattern file.txt

Use [[ ]] Not [ ]

hljs bash
# Bad - word splitting issues
if [ $var = "value" ]; then

# Good - safer
if [[ "$var" == "value" ]]; then

Script Template

hljs bash
#!/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 "$@"
Knowledge Check

Why should you quote variables like $var?

Checklist

Before deploying a script:

  • set -euo pipefail at 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 local in functions, readonly for constants
  • Handle cleanup with trap ... EXIT
  • Use mktemp for 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.