Skip to content
ALB Wiki

Shell Scripting

This is an old revision of this page — revision 1 (2026-10-25 14:49:19 UTC). It may differ from the current revision.

A shell script is a program — the same grammar from chapter 9, saved and executable. This chapter turns the grammar into software: the shebang contract, arguments, control flow, functions, the safety flags, and the idioms real scripts are built from — all in portable POSIX sh, with bash's extensions marked.

The skeleton — every script's preamble

#!/bin/sh
# what this script does — one honest comment line
set -eu                      # -e die on error · -u die on unset vars
IFS='
'                        # optional: strict word-splitting hygiene

#!/bin/sh — the shebang — makes it a program; the kernel reads #! and hands the file to the named interpreter. chmod +x and it's runnable. The set flags are the professional preamble: -e stops at the first failure (no zombie scripts continuing after a cd failed), -u catches typo'd variables, -o pipefail (bash) makes pipelines propagate inner failures. #!/usr/bin/env bash for bash scripts specifically.

Arguments — the script's input

$0          # the script's own name
$1 $2 ...   # positional args
$#          # count
$@          # all args as separate words ("$@" — always quoted)
$*          # all args as one word
shift       # drop $1, everything moves down
if [ "$#" -lt 1 ]; then
    echo "usage: $0 <file>" >&2
    exit 1
fi
for f in "$@"; do ...; done      # "$@" iterates args safely

Variables — scope and habits

var=value              # NO SPACES around =
echo "$var"            # always quoted
readonly VERSION=1.0   # constants
local x                # function-local — bash; POSIX uses distinct names
export PATH="$PATH:/x" # into the environment
cmd=$(command)         # capture output — the universal idiom

Control flow — the complete set

if [ -f "$file" ]; then
    echo exists
elif [ -d "$file" ]; then
    echo dir
else
    echo missing
fi

case "$1" in
    start)   daemon_start ;;
    stop)    daemon_stop ;;
    -h|--help) usage ;;
    *)       echo "unknown: $1"; exit 1 ;;
esac

for f in *.txt; do            # glob-driven
    process "$f"
done
for i in 1 2 3; do echo "$i"; done
for i in $(seq 1 10); do ...; done       # numeric via seq

while IFS= read -r line; do   # the file-reading idiom — THE way to read lines
    printf '%s\n' "$line"
done < input.txt

until [ -f ready ]; do sleep 1; done    # until-loop

The while IFS= read -r line loop is the canonical file iterator — read splits on IFS, -r keeps backslashes, the IFS= prefix preserves whitespace. Never for line in $(cat file) — it word-splits.

Functions

die() { echo "error: $*" >&2; exit 1; }
usage() { cat <<EOF
usage: $0 <args>
EOF
}

backup() {
    local src="$1" dst="$2"     # bash 'local'
    cp -a "$src" "$dst" || die "backup failed: $src"
}

backup a/ b/ || exit 1          # functions return exit codes like commands

The safety machinery

set -eu                    # the preamble
set -x                     # trace — the debugging mode
trap 'cleanup' EXIT        # guaranteed cleanup — THE pattern
trap 'echo "line $LINENO failed"' ERR   # bash ERR trap
trap '' INT                # ignore signals
tmp=$(mktemp -d)           # safe tempdirs — always mktemp, never /tmp/foo.$$
trap 'rm -rf "$tmp"' EXIT

trap ... EXIT is the idiom that separates careful scripts from sloppy ones — temp files, lock files, mounted things — cleaned even on early failure.

printf over echo

printf '%s\n' "$var"       # portable, predictable
printf '%-10s %5d\n' name 42   # formatted columns
echo "$var"                # fine for simple; printf for format control

echo has portability landmines (-e, -n, backslashes); printf has none — use it for anything beyond a plain string.

The idioms that earn their keep

# arg-count check
[ "$#" -ge 1 ] || die "need a file"

# default value
name=${1:-default}

# required command
command -v gcc >/dev/null || die "gcc missing"

# quiet a command
cmd >/dev/null 2>&1

# the lock — prevent double-run
lock=/tmp/myapp.lock
exec 200>"$lock"; flock -n 200 || exit 0

# config override
: "${CONF:=/etc/myapp.conf}"   # bash/POSIX default-assign

# dry-run flag
[ "$DRY" = 1 ] && echo "would rm $f" || rm "$f"

# the pipe-safe read
cat file | while read -r l; do x="$l"; done   # subshell — x dies outside!
while read -r l; do x="$l"; done < file        # same shell — x survives

POSIX vs bash — what you gain and lose

# bash powers worth the shebang
arr=(a b c); echo "${arr[1]}"           # arrays
[[ "$s" =~ ^[0-9]+$ ]]                  # regex test
${var/pat/rep}                          # substitution
(( i++ ))                               # arithmetic syntax
select opt in a b; do ...; done         # menus
declare -A map; map[k]=v                # associative arrays

Write POSIX unless you need the extension — then say #!/bin/bash plainly and use it.

Debugging — the honest workflow

sh -x script.sh            # trace every step — the debugger
bash -n script.sh          # syntax-check without running
set -x; ...; set +x        # trace a region
echo "DEBUG: var=$var" >&2 # printf-style probes to stderr

set -x plus trap plus >&2 diagnostics covers most script debugging without a real debugger.

Where scripts go wrong

SymptomCause
var=x: command not foundSpaces around = — var = x isn't assignment
Loop var empty outside the loop| while = subshell — < file instead
[ $x = 1 ] errors on empty xUnquoted — [ "$x" = 1 ]
Script says "file not found"CRLF line endings — the shebang line has \r
Works in bash, dies in sh#!/bin/sh script with bashisms
Temp file collisionUsed a fixed name — mktemp
Script keeps going after failureMissing set -e
read ate the pipefor+$(cat) instead of while-read

Exercises

  1. Write backup.sh — arg check, cp -a, die on failure, usage text.
  2. The while-read vs for-in-cat difference — prove the subshell loss.
  3. trap a temp dir's cleanup — exit early mid-script, verify it still cleaned.
  4. sh -x a script you wrote — read the trace.
  5. Port a bash script ([[, arrays) to strict POSIX — what's actually needed?

See also

Categories: Handbook · Linux · Linux Handbook · Scripting
This page was last edited 2026-10-25 14:49:19 UTC by ALB Team. What links here