In bash, what value does the special parameter `$?` hold, which value means success, and why does inserting an `echo` between a command and its `$?` check break the check?
answer
- zero is success, non-zero is failure
- refreshed after every command
- echo counts as a command too
- capture it on the very next line
- or just put the command in the if
basics
~20 sIn bash, $? holds the exit status of the most recently completed foreground command: 0 means success and 1-255 mean some kind of failure. Every command resets it, including echo, so an intervening command overwrites the status you meant to test.
solid answer
~50 sEvery command in bash finishes by returning a small integer, its exit status, and bash stores that number in `$?`. The convention is inverted compared with most languages: `0` means success, and any value from `1` to `255` means failure — there is one way to succeed and many ways to fail. The key gotcha is that `$?` is a one-shot value: it is overwritten by the *next* command that completes, and `echo`, `[`, and even a plain assignment all count as commands. So `deploy; echo "done"; if [ $? -ne 0 ]` tests `echo`'s status, which is essentially always 0, and the error check silently never fires. If you need the value later, capture it on the very next line with `rc=$?`; most of the time you don't need `$?` at all, because you can put the command straight into the `if`.
code
bash · 9 lines#!/usr/bin/env bash
grep -q root /etc/passwd
rc=$?
echo "grep finished"
if [ "$rc" -eq 0 ]; then
echo "root found"
else
echo "root missing (status $rc)"
figo deeper
Recall the convention out loud: 0 is success, anything from 1 to 255 is a failure, and $? holds the last command's status. Show that you know echo overwrites it.
Explain the one-shot lifetime precisely — every completed command, including assignments and test commands, rewrites $? — and demonstrate the rc=$? capture on the immediately following line.
Show the review instinct: when you read a script, look at what sits between a command and its status check, and prefer if ! cmd so the check cannot drift as the code grows. Note that non-zero can mean an outcome, not a fault.
Own the convention as an interface. Decide which distinct non-zero codes your scripts publish so that callers, CI steps and monitors can branch on them, and insist that error handling be structural rather than depending on nobody inserting a log line.
## What an exit status is Every command bash runs — an external program, a builtin like `cd`, a shell function, a compound statement — finishes by handing the shell one small integer called its **exit status** (or return code). That integer is the command's only structured way of telling the shell whether it worked. Anything the command *printed* is data on a stream; the status is a separate channel. The values are conventional, not enforced: - `0` — success. - `1` through `255` — failure, with the meaning chosen by the program. This is backwards from most programming languages, where `0` is falsy and non-zero is truthy. The shell convention exists because there is exactly one way for a command to succeed and many distinct ways for it to fail, so the single reserved value goes to success and the rest of the range is free for error codes. ## `$?` and its one-shot lifetime Bash exposes the status of the last completed foreground command through the special parameter `$?`. It is not a variable you set; the shell rewrites it after every command finishes. ```bash ls /nonexistent echo "$?" # 2 (the status ls returned) echo "$?" # 0 (the status of the FIRST echo) ``` The second `echo` already prints `0`, because the first `echo` succeeded and replaced the value. That is the whole trap: **`$?` describes the previous command, whatever that command happened to be.** A log line, a `[` test, even a plain assignment such as `msg="failed"` all complete with their own status and clobber it. ## The bug this produces ```bash deploy_app echo "deploy step finished" if [ "$?" -ne 0 ]; then echo "deploy failed" >&2 exit 1 fi ``` This looks like error handling and is not. By the time `[` runs, `$?` is the status of `echo`, which writes to a terminal or a log file and virtually always succeeds. The script reports success no matter what `deploy_app` did. Reviewers spot this pattern by looking at what physically sits between the command and the test. ## Two correct shapes Capture the status immediately, on the very next line, before anything else can run: ```bash deploy_app rc=$? echo "deploy step finished (status $rc)" if [ "$rc" -ne 0 ]; then exit "$rc"; fi ``` Or — usually better — skip `$?` entirely and let the conditional run the command itself, since bash conditionals consume exit statuses directly: ```bash if ! deploy_app; then echo "deploy failed" >&2 exit 1 fi ``` The second form cannot drift, because nothing can be inserted between the command and the test. ## Status is not output Candidates often conflate the two. A command can print a page of text and still exit `0`; a command can print nothing and exit `1`. `grep -q pattern file` deliberately prints nothing and communicates only through its status. That separation is what makes shell composable: the status drives control flow, the output flows down a pipe or into a variable. ## Non-zero does not always mean "broken" Many tools use distinct non-zero codes to report *outcomes* rather than *errors*. `grep` returns `0` when it matched, `1` when it simply found nothing, and `2` when something actually went wrong such as an unreadable file. `diff` uses `0` for identical, `1` for differing, `2` for trouble. So "non-zero" means "not the success case", and when you care about the difference you must inspect the specific value rather than treating everything non-zero as a crash. Check the tool's own documentation before writing a blanket failure branch around it. ## Other places the number comes from Inside a script, `exit N` sets the script's own status explicitly; with no argument, `exit` reuses the current `$?`. A shell function reports a status too, through its own mechanism, which is covered separately under function return status. An external program's status is whatever it returned from `main`. Bash also reserves a few specific values — notably for a command that could not be found or could not be executed, and for a command terminated by a signal — which are worth knowing when reading CI output. ## What to say in an interview State the convention (`0` success, `1-255` failure), state that `$?` is refreshed by every command so it must be read or saved immediately, and demonstrate that you would normally avoid `$?` by putting the command directly into the conditional.
- How would you print a progress message and still test the earlier command's status?Capture it first: put `rc=$?` on the line immediately after the command, then log freely and test `"$rc"`. Nothing may run between the command and the capture — not an `echo`, not an assignment from a command, not a `[` test. Alternatively drop `$?` altogether and write `if ! cmd; then ...; fi`, which cannot be broken by an inserted line.
- Does a plain variable assignment like `x=5` change `$?`Yes. A bare assignment completes with status 0, so it resets `$?` just like any other command. The exception worth knowing: if the right-hand side runs a command, as in `x=$(some_cmd)`, the assignment takes that command's status instead. Either way, an assignment between a command and its check destroys the value you wanted.
- What is `$?` at the very start of a script, before any command has run?It is `0`. Bash initialises the parameter to success at shell startup, so an early `echo "$?"` prints `0` rather than an error or an empty string. That makes it useless as a sentinel — never treat a `0` as proof that a specific command ran and succeeded, only that the last thing to complete did.
$? is a scoreboard that only ever shows the last play. Glance at it after the wrong play and you are reading someone else's score.
saying these in an interview costs you the question
- Thinks 1 means success, mirroring C-style booleans
- Believes $? persists until something explicitly resets it
- Checks $? after logging a message and expects the original status
- Says exit status is the command's output
- Assumes every failure is exactly 1