Skip to content
ALB Wiki

Diff — Shell

Comparing rev 1 → rev 2

{{infobox
| title = The Shell
| chapter = Linux Handbook, chapter 9 — POSIX sh first, bash where it earns its keep
| core = The command language · the REPL · the OS's front door
| shells = sh (POSIX) · bash · dash · zsh · ksh — the grammar is mostly shared
| skills = pipes · redirection · globbing · quoting · expansion order
| next | Core utilities — the vocabulary the grammar drives
}}
**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
```sh
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:
```sh
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
```sh
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
```sh
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:
```sh
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
```sh
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 test` reveals which live in the shell (`cd`, `echo`, `[`, `export`) vs which are `/bin` programs; `type -a` shows all resolutions. Builtins run in-shell — `cd` *must* be a builtin to change your process's cwd.
- **`exec`** — replaces the shell process; `exec program` never returns; `exec > log` redirects the shell itself
- **`set`** — the shell's own switches: `set -e` die on error, `set -u` die on unset vars, `set -x` trace, `set -o pipefail` (bash) — the script-safety preamble
- **`hash`** — bash caches binary locations; `hash -r` clears it (the LFS `set +h` trick)
- **`trap`** — run code on signals: `trap 'cleanup' EXIT`
- **`eval`** — re-parse a string as shell; powerful and dangerous — quote-sanitized input only
## Environment and startup
```sh
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.
+
## The expansions — the complete taxonomy
+
+
The order matters because each stage feeds the next — this is the full machine:
+
+
```sh
+
{a,b,c}        # 1. brace expansion — a b c — bash only
+
~/x ~user/x    # 2. tilde — home dirs
+
$v ${v}        # 3. parameter expansion — the deep one:
+
  ${v:-default}    # if unset/empty, use default
+
  ${v:=default}    # ...and assign it
+
  ${v:?error msg}  # die if unset — the safety check
+
  ${v:+other}      # if set, use other
+
  ${#v}            # length
+
  ${v#pat}         # strip shortest prefix match
+
  ${v##pat}        # strip longest prefix
+
  ${v%pat}         # strip shortest suffix
+
  ${v%%pat}        # strip longest suffix
+
  ${v/pat/rep}     # substitute — bash
+
  ${v:2:5}         # substring — bash
+
$((1+2*3))     # 4. arithmetic — integers, C-like ops
+
$(cmd)         # 5. command substitution — nests, POSIX
+
`cmd`          #    the legacy form — same thing, uglier
+
<(cmd) >(cmd)  # process substitution — bash: cmd as a file
+
$v word split  # 6. word splitting — unquoted expansions split on IFS
+
*.txt ?.c      # 7. pathname expansion — the globs
+
   *           #    any run (not leading dots)
+
   ?           #    one char
+
   [abc][a-z]  #    classes/ranges
+
   [!a] [^a]   #    negation
+
   globstar ** #    recursive — bash shopt -s globstar
+
'x' "x" \$     # 8. quote removal — after everything
+
```
+
+
The trap order: word-splitting happens *after* parameter expansion — an unquoted `$v` splits on IFS *then* globs — the double hazard `ls $v` suffers.
+
+
## The special variables — $'s secret life
+
+
```sh
+
$$    # this shell's pid
+
$!    # last background job's pid
+
$?    # last exit code — the contract
+
$#    # arg count
+
$0    # the script/shell name
+
$-    # this shell's option flags
+
$@ $* # all args — "each word" vs "one word"
+
$_    # last arg of previous command — bash
+
$PWD $OLDPWD $HOME $PATH $IFS $RANDOM $SECONDS $LINENO
+
```
+
+
## IFS — the splitter
+
+
`IFS` (Internal Field Separator) decides where word-splitting breaks — default `space tab newline`. `IFS=: read -r a b <<< "x:y"` splits on colons; `IFS=$'\n'` splits only on lines — the for-loop safety trick. The `while IFS= read` idiom's `IFS=` prefix prevents leading-whitespace loss per line.
+
+
## Coprocesses, exec-fd magic, and the corners
+
+
```sh
+
exec 3> out.log          # open fd 3 for writing
+
echo hi >&3              # write to it
+
exec 3>&-                # close it
+
exec 8< input.txt        # open for reading
+
read -u8 line            # read from fd 8
+
coproc P { cmd; }        # bash — a process with piped fds
+
cmd1 |& cmd2             # pipe stdout+stderr — bash
+
```
+
+
File-descriptor gymnastics is the shell's deepest layer — redirecting whole script output (`exec > log 2>&1` at the top is the logging boilerplate).
+
+
## POSIX sh vs bash — the extended table
+
+
| Feature | POSIX | bash |
+
| --- | --- | --- |
+
| `[[ ]]`, `=~` regex | ✗ | ✓ |
+
| `<<<` herestring | ✗ | ✓ |
+
| `&>` redirect | ✗ | ✓ |
+
| `{a,b}` braces | ✗ | ✓ |
+
| Arrays, assoc arrays | ✗ | ✓ |
+
| `local`, `declare`, `typeset` | ✗ | ✓ |
+
| `(( ))`, `let` | ✗ | ✓ |
+
| `<(...)`, `>(...)` proc-sub | ✗ | ✓ |
+
| `coproc` | ✗ | ✓ |
+
| `select` | ✗ | ✓ |
+
| `globstar`, `extglob` | ✗ | ✓ |
+
| `${v:offset:len}` | ✗ | ✓ |
+
| `pipefail`, `BASH_SOURCE` | ✗ | ✓ |
+
| `trap ... ERR`, `RETURN` | ✗ | ✓ |
+
| `$(cmd)`, `${v:-d}`, `for/case/while` | ✓ | ✓ |
+
+
## The startup file matrix — which runs when
+
+
| Invocation | Reads |
+
| --- | --- |
+
| login shell (`su -`, console login, `bash -l`) | /etc/profile → ~/.bash_profile (or .profile) |
+
| interactive non-login (new xterm) | ~/.bashrc |
+
| non-interactive (`bash script.sh`) | $BASH_ENV if set |
+
| sh-as-sh | /etc/profile + ~/.profile on login |
+
| POSIX login shell | /etc/profile + ~/.profile |
+
+
The classic confusion: `.bashrc` edits don't affect scripts — scripts aren't interactive and read no files.
+
## 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
1. Predict, then check: `echo $(echo $(echo deep))` — nesting.
2. `ls *.{c,h}` vs `ls "*.txt"` — which is literal?
3. `false && echo no || echo yes` — trace the logic.
4. `set -x; ls nonexistent; set +x` — watch the trace.
5. Write a one-liner proving `cd` must be a builtin.
6. `env -i bash -c 'env'` — what's in a sterile env?
## See also
- [[Linux]] · [[Filesystem]] · [[Core utilities]] · [[Shell scripting]]
[[Category:Linux]]
[[Category:Handbook]]
[[Category:Shells]]