| | {{infobox |
| | | title = Shell Scripting |
| | | chapter = Linux Handbook, chapter 14 — POSIX programs: variables, control flow, functions, the discipline |
| | | core | A script is a program — shebang, exit codes, arguments, control flow |
| | | dialects = POSIX sh (portable) · bash (the capable superset) |
| | | skills | quoting hygiene · set flags · traps · functions · the idioms |
| | | next | The C toolchain — where scripts end and compilers begin |
| | }} |
| | |
| | **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 |
| | |
| | ```sh |
| | #!/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 |
| | |
| | ```sh |
| | $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 |
| | ``` |
| | |
| | ```sh |
| | if [ "$#" -lt 1 ]; then |
| | echo "usage: $0 <file>" >&2 |
| | exit 1 |
| | fi |
| | for f in "$@"; do ...; done # "$@" iterates args safely |
| | ``` |
| | |
| | ## Variables — scope and habits |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | # 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 |
| | ``` |
| | |
| | ## getopts — parsing flags |
| | |
| | ```sh |
| | usage() { echo "usage: $0 [-v] [-o out] files..."; } |
| | verbose=0; out= |
| | while getopts "vo:h" opt; do |
| | case "$opt" in |
| | v) verbose=1 ;; |
| | o) out="$OPTARG" ;; |
| | h) usage; exit 0 ;; |
| | *) usage; exit 1 ;; |
| | esac |
| | done |
| | shift $((OPTIND - 1)) # remaining args in $@ |
| | ``` |
| | |
| | `getopts` is POSIX — the `-v`, `-o arg` grammar handled properly. bash's `getopt` (no s) and manual `while`/`case` on `$1` handle `--long-flags` where getopts doesn't. |
| | |
| | ## The safe-script template — the professional skeleton |
| | |
| | ```sh |
| | #!/bin/sh |
| | # what this does — one honest line |
| | set -eu # die on error, unset vars |
| | umask 022 |
| | tmp=$(mktemp -d) # a private tempdir |
| | trap 'rm -rf "$tmp"' EXIT # guaranteed cleanup |
| | trap 'echo "fail at line $LINENO" >&2' ERR # bash only |
| | |
| | die() { printf 'error: %s\n' "$*" >&2; exit 1; } |
| | require() { command -v "$1" >/dev/null 2>&1 || die "$1 required"; } |
| | |
| | require tar |
| | require awk |
| | |
| | main() { |
| | [ "$#" -ge 1 ] || die "need an argument" |
| | for f in "$@"; do |
| | [ -f "$f" ] || die "not a file: $f" |
| | process "$f" |
| | done |
| | } |
| | main "$@" |
| | ``` |
| | |
| | The pieces: `set -eu`, `mktemp -d` + `trap`, `die()`, `require()` for tool checks, `main "$@"` as the entry — the shape every serious script takes. |
| | |
| | ## The signal traps — the shutdown machinery |
| | |
| | ```sh |
| | trap 'cleanup' EXIT # runs on any exit — even errors — THE idiom |
| | trap 'kill $pid' TERM # graceful shutdown |
| | trap '' INT # ignore Ctrl-C — the don't-interrupt-us zone |
| | trap 'echo got sigusr1' USR1 |
| | trap - INT # reset to default |
| | ``` |
| | |
| | ## The debugging toolkit |
| | |
| | ```sh |
| | sh -x script.sh # trace — THE debugger |
| | sh -n script.sh # syntax-only check |
| | bash -v script.sh # print lines as read |
| | set -x ... set +x # trace a region |
| | PS4='+ $LINENO: ' sh -x script.sh # line-numbered trace |
| | shellcheck script.sh # the linter — the real debugging tool |
| | ``` |
| | |
| | `shellcheck` catches the quoting bugs, the `for x in $(ls)`, the unguarded `rm` — run it on every script. |
| | |
| | ## The idioms library |
| | |
| | ```sh |
| | # default value |
| | : "${VAR:=default}" |
| | # required var |
| | : "${VAR:?must set VAR}" |
| | # command exists |
| | command -v cmd >/dev/null || die "missing cmd" |
| | # retry n times |
| | for i in 1 2 3; do cmd && break || sleep 1; done |
| | # run-as-user |
| | sudo -u svcacct script.sh |
| | # the lock — only one instance |
| | exec 9>/tmp/myapp.lock; flock -n 9 || exit 0 |
| | # atomic write |
| | tmp=$(mktemp) && write > "$tmp" && mv "$tmp" target |
| | # the here-doc to file |
| | cat > file <<'EOF' |
| | content — quoted EOF = literal, unquoted = expands |
| | EOF |
| | # parallel jobs with wait |
| | for x in a b c; do work "$x" & done; wait |
| | # read config key=val pairs |
| | while IFS='=' read -r k v; do case "$k" in name) NAME="$v";; esac; done < cfg |
| | ``` |
| | |
| | ## POSIX vs bash — what you gain and lose |
| | |
| | ```sh |
| | # 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 |
| | 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 |
| | |
| | | Symptom | Cause | |
| | | --- | --- | |
| | | `var=x: command not found` | Spaces around `=` — `var = x` isn't assignment | |
| | | Loop var empty outside the loop | `|` while = subshell — `< file` instead | |
| | | `[ $x = 1 ]` errors on empty x | Unquoted — `[ "$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 collision | Used a fixed name — `mktemp` | |
| | | Script keeps going after failure | Missing `set -e` | |
| | | `read` ate the pipe | for+$(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 |
| | |
| | - [[Shell]] · [[Text processing]] · [[Core Utilities]] · [[Linux]] |
| | |
| | [[Category:Linux]] |
| | [[Category:Handbook]] |
| | [[Category:Scripting]] |
| + | [[Category:Linux Handbook]] |
| | |