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
01
Getting started
- 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. Start →
- 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. Start →
- 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. Start →
- 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. Start →
- 05 Functions, local, and returning values A function returns an exit status (0–255). To return data, print it and capture with $( ). Start →
02
Expansion
- 06 The expansion pipeline Quoting does not stop expansion. It stops the two stages that come after it — word splitting and pathname expansion. Start →
- 07 Single quotes, double quotes, and neither Single quotes stop everything. Double quotes stop splitting and globbing but still allow $, ` and \. Start →
- 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. Start →
- 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. Start →
- 10 Arithmetic, and the base-8 trap Bash arithmetic is integer only. A leading zero means octal, so $((08)) is an error, not eight. Start →
- 11 Globbing is not regex The shell expands globs, not the command. By the time find or grep runs, the pattern is already gone. Start →
03
Working with data
- 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. Start →
- 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. Start →
- 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. Start →
- 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) ). Start →
- 16 Here-documents and here-strings <<EOF expands variables inside the block. <<'EOF' does not. Quoting the delimiter is the switch. Start →
04
Input & output
- 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. Start →
- 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. Start →
- 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. Start →
- 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. Start →
- 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. Start →
- 22 Arguments and getopts getopts handles short options only. Long options like --verbose need a manual loop — getopts will never do them. Start →
05
Making it robust
- 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. Start →
- 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. Start →
- 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. Start →
- 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. Start →
- 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. Start →
- 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. Start →