Shell & Command-Line Engineering
Begin with one three-line shell result, then add one command-language boundary at a time. Twelve chapters teach quoting, parameters, decisions, functions, streams, files, processes, interfaces, tests, portability, security, and recovery without assuming Bash extensions, administrator access, packages, servers, containers, cloud accounts, or production systems.
Your first shell program: command, output, and exit status
Objective Save and run one POSIX shell file, predict its three output lines, and distinguish command text, process output, and terminal status.
Core explanation
A shell is both an interactive command reader and a language interpreter. It parses source into commands and words, performs defined expansions, starts or invokes commands, connects their standard streams, and records an exit status. Standard output carries the requested result; standard error carries diagnostics; the numeric status tells the caller whether the command fulfilled its contract. The first program uses only POSIX syntax and built-ins. It asserts its own arithmetic values before printing three stable lines. This proves one local interpreter path and exact result, not that every shell, operating system, installed command, permission model, or production environment behaves identically.
Separate the terminal, shell, command, and process
A terminal is an input and display interface. A shell reads commands and may run built-ins inside its own process or start another executable as a child process. The source file is inert text until a selected interpreter reads it. Keeping these nouns separate lets a beginner explain whether a failure happened while locating an interpreter, parsing source, expanding words, starting a command, or executing the command body.
Interactive history is convenient but weak evidence because earlier directory, variable, option, and alias state can change the result. A saved script plus an explicit invocation establishes source identity and order. Begin in a disposable directory, inspect the interpreter command, and run the same file from a clean shell. A shebang matters when the operating system executes the file directly; `sh main.sh` instead chooses the named `sh` command explicitly.
Treat output, diagnostics, and status as independent results
Standard output is a byte stream for requested results, while standard error is a separate byte stream for diagnostics. Both can appear in one terminal window even though a caller can redirect them independently. An exit status is a small integer retained by the parent after the command terminates. Zero conventionally means the command fulfilled its contract; a nonzero value needs a documented category rather than a universal interpretation.
Capture each channel without destroying the one you need to inspect. A script can print attractive output and still return failure, or return zero after swallowing a serious error. Conversely, a useful warning on standard error does not necessarily make the requested result invalid unless the interface says so. Tests should compare exact stdout, exact or bounded stderr, status, and owned side effects separately instead of treating terminal appearance as one undifferentiated result.
Understand strict options without cargo-culting them
`set -e` requests exit after certain unhandled failures, and `set -u` rejects expansion of unset parameters. They catch useful mistakes but have grammatical exceptions around conditions, lists, pipelines, and functions. They do not replace explicit validation, rollback, cleanup, or status checking. A command whose nonzero result is an expected branch belongs inside `if`, `while`, `until`, `&&`, or `||` with a named interpretation.
Do not paste a long strict-mode incantation without understanding the supported shell. POSIX `sh` has no standard pipefail option, and Bash-specific options cannot be assumed in a portable script. Start from `set -eu`, keep expected failures in explicit conditional positions, and add interpreter-specific protection only after feature detection and tests on the declared runtime matrix.
Laboratory: classify the first failing boundary
Save the Chapter 1 program exactly, run `sh -n main.sh`, then run `sh main.sh` with stdout and stderr captured separately and inspect the status immediately. Introduce one unmatched quote, one failing assertion, one unwritable owned destination, and one unknown command in separate restored copies. Predict whether parsing begins, any process output appears, cleanup is needed, and which channel records evidence.
The syntax fault must fail before ordinary execution. The assertion can fail only after parsing and earlier commands. An unwritable destination reaches redirection setup before the target command body. An unknown command reaches command lookup after parsing and expansion. Preserve the first divergent stage and status for each case, restore the baseline between runs, and never describe every nonzero outcome as “the shell crashed.”