Shell
The shell is the operating system's front door — the program that reads your commands, expands them into what the kernel executes, and returns what happened. Everything else in this handbook runs through it. This chapter teaches the POSIX shell grammar — the portable core every sh speaks — with bash's extensions flagged where they genuinely earn their keep. Write POSIX, reach for bash deliberately: that's the discipline.
Which shell is which
echo $SHELL # your login shell
ls -l /bin/sh # WHAT sh actually is on this system — varies!
/bin/sh is a symlink: to bash on Slackware, dash on Debian, bash in POSIX mode elsewhere. The distinction matters — scripts shebang'd #!/bin/sh must be portable (no bashisms), while #!/bin/bash buys the extensions. dash exists specifically to be the fast, strict POSIX sh that doesn't forgive sloppy scripts.
The grammar — the eight expansions
When you hit enter, the shell transforms your line in a fixed order — learn it once:
echo ~ $HOME $((1+2)) $(uname) *.txt {a,b} "quoted" 'literal'
# 1 brace 2 tilde 3 param+arith+cmdsubst 4 word-split 5 glob 6 quote-removal
Each stage is a tool and a trap:
$var/${var}— parameter expansion;${var:-default},${var#prefix},${var%suffix},${#var}string ops are POSIX$(cmd)— command substitution, nestable, POSIX; backticks are the legacy form$((expr))— arithmetic, integer only*.txt— globbing:*any run,?one char,[abc]/[a-z]classes,[!a]negation{a,b,c}— brace expansion — bash only, not POSIX- Tilde —
~,~user - Quotes —
'...'fully literal;"..."protects spaces but allows$;"$@"is the special case
Quoting — where every shell bug lives
f="my file.txt"
ls $f # TWO words — word splitting — the classic bug
ls "$f" # one word — quote variables ALWAYS
echo '$HOME' # literal $HOME — single quotes stop everything
echo "$HOME" # expands
The rule that never lies: quote every variable expansion unless you specifically want word-splitting. rm -rf $dir unquoted is how systems die; rm -rf "$dir" survives spaces.
Redirection — the complete grammar
cmd > out # stdout to file (truncate)
cmd >> out # append
cmd 2> err # stderr
cmd > out 2>&1 # both to one file — order matters!
cmd &> out # bash shorthand for both
cmd < in # stdin from file
cmd << EOF # heredoc — literal input until EOF
cmd <<< "str" # herestring — bash only
cmd | next # stdout → stdin — the pipe
cmd1 && cmd2 # run second only if first succeeds
cmd1 || cmd2 # run second only if first fails
cmd ; next # sequence
cmd & # background
2>&1 ordering is the subtle one — > out 2>&1 binds stderr to stdout's destination; 2>&1 > out binds stderr to the old stdout (terminal). Order is everything.
Exit codes — the contract
Every command returns 0–255: 0 = success, nonzero = failure classes. $? reads the last one:
grep -q match file && echo found
[ -f file ] && echo exists # test — the conditional primitive
test -d dir || mkdir dir
The conditional builtins: [/test (POSIX) — -f/-d/-r/-w/-x, -z/-n, =/!=, -eq/-lt/gt. bash's [[ ]] adds pattern matching and &&/|| inside — not POSIX but genuinely better.
Job control
cmd & # background it
jobs # list
fg %1 # foreground job 1
bg %1 # resume in background
Ctrl-Z # suspend
Ctrl-C # SIGINT — interrupt
Ctrl-D # EOF — end stdin
nohup cmd & # survive hangup
Special shell powers
- Builtins vs binaries —
type cd echo testreveals which live in the shell (cd,echo,[,export) vs which are/binprograms;type -ashows all resolutions. Builtins run in-shell —cdmust be a builtin to change your process's cwd. exec— replaces the shell process;exec programnever returns;exec > logredirects the shell itselfset— the shell's own switches:set -edie on error,set -udie on unset vars,set -xtrace,set -o pipefail(bash) — the script-safety preamblehash— bash caches binary locations;hash -rclears it (the LFSset +htrick)trap— run code on signals:trap 'cleanup' EXITeval— re-parse a string as shell; powerful and dangerous — quote-sanitized input only
Environment and startup
export VAR=val # into the environment — children inherit
env # dump it
env -i cmd # the sterile env — LFS's tool
Startup files differ by invocation: login shells read /etc/profile + ~/.profile (POSIX) — bash adds ~/.bash_profile; interactive non-login read ~/.bashrc; non-interactive (scripts) read neither — which is why a variable set in .bashrc doesn't exist inside a script. su - user gives a login shell; su user doesn't — the - matters.
POSIX vs bash — the honest table
| Feature | POSIX | bash |
|---|---|---|
$(cmd), >, |, && | ✓ | ✓ |
[[ ]], <<<, &> | ✗ | ✓ |
{a,b} braces | ✗ | ✓ |
Arrays arr=(...) | ✗ | ✓ |
local, declare | ✗ | ✓ |
${var/pat/rep}, regex =~ | ✗ | ✓ |
pipefail, BASH_SOURCE | ✗ | ✓ |
Write POSIX for portability; use bash deliberately for arrays/[[/regex — and say so in the shebang.
Where shells go wrong
| Symptom | Cause |
|---|---|
| Variable empty in script, set in .bashrc | Startup files don't run non-interactively |
command not found right after install | bash hash cache — hash -r or set +h |
rm: no such file on a file that exists | Unquoted var word-split on spaces |
2>&1 captures nothing | Order wrong — > file 2>&1 not 2>&1 > file |
| Script works in bash, fails in sh | Bashisms — [[, arrays, <<< in a #!/bin/sh script |
| Glob didn't expand | Quoted it — "*.txt" is literal |
| Ctrl-C doesn't kill it | SIGINT ignored/trapped — try kill -9 |
Exercises
- Predict, then check:
echo $(echo $(echo deep))— nesting. ls *.{c,h}vsls "*.txt"— which is literal?false && echo no || echo yes— trace the logic.set -x; ls nonexistent; set +x— watch the trace.- Write a one-liner proving
cdmust be a builtin. env -i bash -c 'env'— what's in a sterile env?