In a bash script, what does appending & to a command do, what does the special parameter $! hold afterwards, and what does a bare wait do?
answer
- runs it, doesn't stop for it
- status is 0 before anything finished
- a one-slot register holding a PID
- overwritten by the next background job
- blocking until the children are done
basics
~20 sAppending & runs the command asynchronously in a background child process and returns immediately with status 0. $! then holds that child's PID. A bare wait blocks until every background child of the script has finished.
solid answer
~40 s`cmd &` starts `cmd` in a forked child and does **not** wait for it — the shell moves straight to the next line, and the status of the asynchronous command itself is always 0, so you learn nothing yet about whether `cmd` worked. Immediately afterwards `$!` expands to the PID of that child, and it is overwritten by the next `&`, so you capture it right away, usually into an array. `wait` with no arguments blocks until all of the script's currently running background children have terminated; `wait "$pid"` blocks for one child and returns *its* exit status. A script that backgrounds work and then falls off the end without waiting simply exits while the children are still running, which in CI usually means truncated output or work killed mid-flight.
code
bash · 13 lines#!/usr/bin/env bash
# Launch two jobs, capture each PID at once, then collect them.
sleep 2 &
pid_a=$!
sleep 1 &
pid_b=$!
echo "started $pid_a and $pid_b"
wait "$pid_a"
echo "a finished with $?"
wait "$pid_b"
echo "b finished with $?"go deeper
Be able to say plainly that & runs a command in the background without waiting, that $! is the PID of that job, and that wait blocks until background children finish. Show the three-line capture idiom.
Explain that the asynchronous command's own status is always 0, that $! is overwritten by the next &, and that for a backgrounded pipeline $! names the last command in it. Know that wait only works on children of this shell.
Show why a script that exits without waiting produces truncated CI logs and work killed mid-flight, and treat PID capture into an array as the default shape of any fan-out script you would put in production.
Frame & as introducing supervision debt: every forked child needs an owner that collects it, a bound on how many exist, and a defined teardown path. Be ready to argue when a shell script should not be the thing doing the supervising at all.
## What `&` actually does In bash, `&` is a *command terminator*, exactly like `;` — it ends the command in front of it. The difference is that `;` runs the command and waits for it, while `&` runs it **asynchronously**: bash forks a child, the child runs the command, and the parent shell returns immediately to read the next line. Two consequences follow directly and both surprise people: 1. **The exit status you see is not the command's.** An asynchronous command's status is 0 the moment it is launched, because nothing has finished yet. `cmd & echo $?` prints 0 even if `cmd` is going to fail two seconds later. 2. **The command runs in its own process**, so anything it changes about the shell — variables, the current directory — is not visible to the parent afterwards. (The mechanics of that isolation are the subshell topic; here it is enough to know a background job is a separate process.) In a non-interactive script, job control is off by default, so bash does not print the `[1] 12345` job line you see at an interactive prompt. The job still exists; `jobs` and `jobs -p` list it from within the same shell. ## `$!` — the PID of the last background job After `&`, the special parameter `$!` expands to the process ID of the job just placed in the background. It is a single value that is **overwritten by the next `&`**, which is why the idiom is always to read it on the same line or the very next one: ```bash long_task a & pid_a=$! long_task b & pid_b=$! ``` If you background two jobs and only then read `$!`, you have the second PID and have permanently lost the first — there is no way to recover it later. For fan-out, collect into an array: `pids+=("$!")`. For a backgrounded **pipeline** such as `producer | consumer &`, `$!` is the PID of the *last* command in the pipeline (`consumer`). The whole pipeline is one job, but `$!` names only that one process. ## `wait` `wait` is a shell builtin — it can only wait for children of *this* shell, which is why you cannot `wait` for an arbitrary PID on the machine (you get status 127 instead). - `wait` with no arguments: block until every currently running background child has terminated. - `wait "$pid"`: block until that one child terminates, and return **its** exit status. - If the child was killed by a signal, the status is 128 plus the signal number (137 for SIGKILL). ```bash sleep 5 & pid=$! wait "$pid" echo "sleep finished with $?" ``` ## Why a script needs `wait` at all If the script reaches its last line while children are still running, bash exits anyway. The children do not die with it — they keep running, now with no parent shell tracking them. That produces the classic CI symptom: the job "passes" in ten seconds because the real work was still in flight when the script returned, and its output never made it into the log. Worse, whatever supervises the script (a CI runner, a container entrypoint, a systemd unit) may tear down the whole process group a moment later and kill the work half-done. So the shape of every fan-out script is: start the children, remember their PIDs, and `wait` before doing anything that depends on their results — including exiting. ## Related things `&` is *not* - `&` is not `&&`. `a & b` runs `a` in the background and then runs `b` immediately; `a && b` runs `b` only if `a` succeeded. - `&` does not make anything faster on its own. It overlaps waiting time; if the work is CPU-bound and you background more jobs than you have cores, you mostly add contention. - `&` alone does not detach a job from the terminal or from the script's process group — `nohup` and `disown` address that, and they are a different question. ## The mental model Treat `&` as "fork and forget", `$!` as a one-slot register holding the last fork's PID, and `wait` as the only way to turn that fork back into a result. A script that uses `&` without `$!` and `wait` has started work it cannot supervise.
- If you background a pipeline with `producer | consumer &`, whose PID ends up in `$!`?The last command of the pipeline — `consumer`. Bash treats the pipeline as a single job, but `$!` names only its final process, and `wait "$!"` therefore returns that command's status, not the producer's. If you need the producer's outcome too, that is what `pipefail` and `PIPESTATUS` are for.
- Why must `$!` be read immediately after the `&` rather than later in the script?`$!` holds only the most recently backgrounded job's PID and is overwritten by the next `&`. Once you background a second job, the first PID is gone for good — there is no history. The safe idiom is to assign it on the spot, typically appending to an array: `cmd & pids+=("$!")`.
- What happens if a script exits while its background children are still running?Bash exits without killing them; the children keep running, now orphaned from the script that started them. Their output may never reach the log, and whatever supervises the script may tear down the process group and kill them mid-work. If you care about the result, `wait` for them; if you do not, kill them explicitly.
saying these in an interview costs you the question
- Says $! holds the background job's exit status
- Thinks & by itself makes a command run faster
- Reads $! after launching a second background job
- Believes the script blocks until background work finishes
- Confuses & with && in a command chain