Skip to content
Bash, properly
~/justmeandlinux

Learn/ground up to senior

Bash, properly

The shell as a language with rules. You will type the expansion, assemble the quoting, and predict what the script prints before you are told.

0% mastered
Start lesson 1
28 lessons · 318 exercises · 346 facts to master
01

Getting started

  1. 01 The shebang, exit codes and $? Zero is success. Every other number is a failure — and $? holds the status of the last command only, so capture it immediately or lose it. Basics · 12 facts · ~5 min Start →
  2. 02 Variables, and why quoting is not optional An unquoted $var is not a value. It is a request to split it on whitespace and then glob whatever falls out. Quoting · 12 facts · ~6 min Start →
  3. 03 Conditionals: [ ], [[ ]] and (( )) [ is a command. [[ is bash syntax — which is why it never word-splits, never globs, and cannot be broken by an empty variable. Control flow · 13 facts · ~6 min Start →
  4. 04 Loops, and the one that eats your variables A while read loop on the right of a pipe runs in a subshell. Every variable it sets is gone the moment the loop ends. Control flow · 13 facts · ~6 min Start →
  5. 05 Functions, local, and returning values A function returns an exit status (0–255). To return data, print it and capture with $( ). Control flow · 12 facts · ~6 min Start →
02

Expansion

  1. 06 The expansion pipeline Quoting does not stop expansion. It stops the two stages that come after it — word splitting and pathname expansion. Expansion · 7 facts · ~7 min Start →
  2. 07 Single quotes, double quotes, and neither Single quotes stop everything. Double quotes stop splitting and globbing but still allow $, ` and \. Quoting · 12 facts · ~6 min Start →
  3. 08 Parameter expansion: defaults, trimming, replacing Anything you were about to do with sed, basename or cut on a single variable can probably be done in ${ } — with no fork. Expansion · 15 facts · ~7 min Start →
  4. 09 Command substitution and subshells Every $( ), every pipe stage and every ( ) is a subshell — a forked copy. Variables set inside one never come back out. Expansion · 12 facts · ~6 min Start →
  5. 10 Arithmetic, and the base-8 trap Bash arithmetic is integer only. A leading zero means octal, so $((08)) is an error, not eight. Control flow · 12 facts · ~5 min Start →
  6. 11 Globbing is not regex The shell expands globs, not the command. By the time find or grep runs, the pattern is already gone. Quoting · 13 facts · ~6 min Start →
03

Working with data

  1. 12 Arrays, and the only safe way to expand one There is one correct expansion and it is "${arr[@]}". Everything else joins, splits or silently drops elements. Data · 14 facts · ~6 min Start →
  2. 13 Associative arrays An associative array must be declared with declare -A first. Without it, bash treats every key as an arithmetic expression and they all collapse to index 0. Data · 14 facts · ~5 min Start →
  3. 14 IFS, read -r, and reading a file safely Every word of while IFS= read -r line; done < file earns its place: IFS= keeps whitespace, -r keeps backslashes, the redirect avoids a subshell. I/O · 13 facts · ~6 min Start →
  4. 15 mapfile: lines straight into an array mapfile -t arr < file reads lines into an array without word splitting. It is the correct replacement for arr=( $(cmd) ). Data · 10 facts · ~4 min Start →
  5. 16 Here-documents and here-strings <<EOF expands variables inside the block. <<'EOF' does not. Quoting the delimiter is the switch. I/O · 12 facts · ~5 min Start →
04

Input & output

  1. 17 Redirection order, and why 2>&1 >file is wrong 2>&1 means "make stderr point wherever stdout points at this moment". Order it after the stdout redirect, not before. I/O · 13 facts · ~6 min Start →
  2. 18 File descriptors and exec exec 3>file opens a descriptor for the rest of the script. exec >file with no number redirects the script's own output from that point on. I/O · 14 facts · ~6 min Start →
  3. 19 Pipes, pipefail and PIPESTATUS A pipeline's exit status is that of its last command. false | true succeeds — which is what set -o pipefail exists to fix. I/O · 12 facts · ~6 min Start →
  4. 20 Process substitution <(cmd) gives a filename that reads a command's output. It is how you feed two commands to diff, and how you avoid the subshell a pipe would create. I/O · 11 facts · ~5 min Start →
  5. 21 stdout is data, stderr is everything else Put data on stdout and everything else on stderr. Anything a caller might pipe belongs on stdout; anything they would want to see while piping belongs on stderr. I/O · 13 facts · ~4 min Start →
  6. 22 Arguments and getopts getopts handles short options only. Long options like --verbose need a manual loop — getopts will never do them. Control flow · 13 facts · ~5 min Start →
05

Making it robust

  1. 23 set -e, and why it lies to you set -e is worth having and is not a safety net. It ignores failures in conditions, in most function calls, and in every pipeline stage but the last. Robustness · 12 facts · ~7 min Start →
  2. 24 Traps, signals and cleaning up trap cleanup EXIT runs on every normal exit and on most signals. It is the one line that makes a script tidy up after itself. Robustness · 13 facts · ~6 min Start →
  3. 25 Temp files, mktemp and races Never construct a temp filename yourself. mktemp creates it atomically, which is the entire point — a predictable name is a symlink attack waiting to happen. Robustness · 12 facts · ~5 min Start →
  4. 26 Locking with flock A lock file you create and check yourself is a race. flock uses a kernel lock, which is released automatically when the process dies — even on kill -9. Robustness · 12 facts · ~5 min Start →
  5. 27 Forking, builtins and why your script is slow Every external command is a fork and an exec. In a loop of 10,000, that is 10,000 processes — and a builtin does the same job for free. Robustness · 13 facts · ~5 min Start →
  6. 28 Portability, bashisms and shellcheck Decide whether you are writing bash or POSIX sh, put the matching shebang on it, and let shellcheck hold you to it. Robustness · 12 facts · ~6 min Start →