Before shipping a bash script you run `bash -n deploy.sh` and it prints nothing. What has bash actually verified, what has it explicitly not done, and which classes of bug does that check still miss?
answer
- parse only, execute nothing
- catches grammar, not meaning
- untaken branches are still parsed
- eval strings are invisible to it
- it parses as bash, not as sh
basics
~20 sbash -n reads and parses the whole script without executing any of it, reporting only syntax errors such as an unclosed quote or a missing fi. It cannot see runtime problems: missing commands, unset variables, bad flags, wrong logic, or code built at runtime and passed to eval.
solid answer
~50 s`bash -n` is a parse-only pass: bash reads the entire file, builds the command structures, and reports the first syntax error it hits, but executes nothing — no files are written, no commands run. That makes it a cheap guard against the failure mode where a script blows up halfway through on an unterminated `"`, a missing `done` or a stray `fi`, leaving the system half-changed. What it cannot tell you is anything that only exists at run time: whether `jq` is installed, whether `$TARGET` is set, whether a flag is spelled the way GNU or BSD expects it, or whether the logic is right. It also cannot see inside strings that become code later — anything you build up and hand to `eval` or `sh -c` is just a string to the parser. So `-n` answers "will bash be able to read this?", and a linter such as ShellCheck answers the separate question of whether the code is *correct*.
code
bash · 9 linescat > /tmp/demo.sh <<'EOF'
#!/usr/bin/env bash
for f in *.log; do
gzip "$f"
EOF
# Parse only: reports the missing `done`, runs nothing, gzips nothing.
bash -n /tmp/demo.sh
echo "exit status: $?"go deeper
Know that bash -n script.sh checks the file for syntax errors and runs nothing, and that silence means it parsed. Be able to name one thing it finds, such as a missing done or an unclosed quote.
Draw the line clearly: it validates grammar, not meaning. Give concrete misses — a misspelled command, an unset variable, a wrong sed flag, code inside an eval string — and note that untaken branches are still parsed.
Explain why it matters operationally: a syntax error at line 380 of a deploy script otherwise fires after 379 lines of side effects. Position it as one cheap gate among several, and say what each of the others actually answers.
Own the release policy for shell in your estate: which checks are mandatory before a script can run against production, who owns the escape hatch, and where the cost of a half-executed script justifies a dry-run mode or a different tool entirely.
## What `-n` does `bash -n script.sh` sets the `noexec` option: bash reads commands but does not execute them. The whole file is lexed and parsed into the shell's internal command structures; if the grammar does not fit, bash prints a syntax error naming the file and line and exits non-zero. If it parses, `-n` is silent and exits 0. Equivalently, `set -o noexec`. In a script you would never set it at runtime — it would stop the script from doing its job — so it is essentially an invocation-time check. ## Why parse-only is worth doing at all A shell script is interpreted line by line as it runs. There is no compile step, so a syntax error near the bottom of a 400-line deploy script is not discovered until the top 350 lines have already run — services stopped, files moved, a database migrated — and then the shell dies mid-way. `bash -n` moves that discovery to before the first side effect. It costs milliseconds, needs no dependencies, and is the one check available even on a machine with no tooling installed. It catches exactly the parse-level defects: ```bash if [ -f "$config ]; then # unterminated quote echo found fi for f in *.log; do gzip "$f" # missing done ``` Both are reported without running anything. It also parses branches that never execute, so a syntax error inside a rarely-taken `case` arm is still found — which is the sort of bug that otherwise surfaces at 3 a.m. the first time that arm is reached. ## What it cannot catch The boundary is precise: `-n` knows grammar, not meaning. - **Missing commands.** `kubctl apply -f x.yaml` parses perfectly; it is a valid command name that happens not to exist. - **Unset or empty variables.** `rm -rf "$PREFIX/lib"` is well-formed whether or not `PREFIX` has a value. - **Wrong options.** `sed -i '' -e s/a/b/ file` parses on any system, and fails at run time on GNU sed. - **Logic.** An inverted condition, an off-by-one in a range, a `>` where you meant `>>` — all grammatical. - **Quoting bugs.** `for f in $files` parses; that it word-splits on spaces is a semantic defect the parser has no opinion about. - **Deferred code.** Anything that becomes code later is a plain string at parse time: ```bash cmd='if [ -f x; then echo hi; fi' # broken shell code bash -n # ...but this file parses fine eval "$cmd" # the error appears only here ``` The same is true of a command string sent to `ssh host "..."` or `sh -c "..."`, and of a here-document whose body is a script for another interpreter. - **Which dialect.** `bash -n` parses the file *as bash*. Running it against a script whose shebang says `#!/bin/sh` proves nothing about whether a POSIX shell will accept it, because bash accepts constructs those shells do not. ## Combining it with `-v` `bash -nv script.sh` prints each line as it is read while still executing nothing. On a file with a syntax error that is genuinely useful: the last lines echoed show how far the parser got before it gave up, which is often more informative than the reported line number — an unterminated quote is reported where the parser finally notices, not where the quote opened. ## Where it sits in a real workflow Think of three distinct gates, answering three different questions: 1. `bash -n` — *can bash read this file at all?* Instant, zero dependencies, no false positives. 2. A shell linter — *is this code likely to be wrong?* It reasons about quoting, exit status, unreachable code and common footguns. Different question, different tool, and it belongs to a separate discussion. 3. Actually running it, ideally against a disposable target — *does it do the right thing?* The only gate that can answer the semantic question. And a related habit that costs nothing: for a destructive script, a `--dry-run` mode that prints the commands it *would* run is the only way to answer the third question without paying for it. `bash -n` cannot do that, because it does not run your code — a dry-run mode is code you write yourself.
- Why does `bash -n` still find a syntax error inside a `case` arm that never runs?Because parsing happens for the whole file before execution: bash builds the command structures for every branch, function body and loop regardless of whether control ever reaches them. That is precisely why it catches the rare-path syntax errors that otherwise surface only in production.
- You run `bash -n` on a script whose shebang is `#!/bin/sh`. What have you proved?Only that bash can parse it. Bash accepts `[[ ]]`, arrays, `local` and process substitution that a stricter POSIX shell rejects, so a clean `bash -n` says nothing about whether dash or another /bin/sh will run it. Check it with the interpreter that will actually execute it.
- What does adding `-v` to a `-n` run get you?`bash -nv` echoes every input line as it is read while still executing nothing, so you can see exactly how far the parser got. That matters for errors such as an unterminated quote, where the reported line number is where bash finally noticed, not where the problem started.
saying these in an interview costs you the question
- Claims bash -n runs the script in a safe sandbox mode
- Thinks it verifies that every command exists
- Expects it to catch unquoted variables or unset variables
- Believes a clean -n means a /bin/sh script is portable
- Assumes it inspects code passed to eval