In bash, what does `shopt -s lastpipe` change about how a pipeline is executed, what condition must hold for it to take effect, and when would you rely on it in a production script?
answer
- only the final stage is affected
- earlier stages must still fork
- there is a precondition about job control
- scripts satisfy it, prompts do not
- bash 4.2 and no POSIX equivalent
basics
~20 sWith lastpipe enabled, bash runs the final command of a pipeline in the current shell instead of a forked subshell, so its variable assignments survive. It only applies when job control is inactive, which is the default in scripts but not at an interactive prompt.
solid answer
~60 sBy default bash forks every stage of a pipeline, which is why a trailing `while read` loop loses whatever it assigns. `shopt -s lastpipe` makes the *last* stage — and only the last — run in the calling shell, so `some-cmd | while read -r x; do n=$((n+1)); done` leaves `n` set afterwards. The precondition is that job control is not active: scripts run with job control off, so it works there, but at an interactive prompt you would first need `set +m` or nothing changes. It arrived in **bash 4.2**, so it is unavailable on the bash 3.2 that macOS ships and meaningless in `dash` or POSIX `sh`. I would generally not reach for it: it is a file-wide change to what every pipeline means, and a plain `done < file` or `done < <(cmd)` fixes the same problem locally and portably. Where it earns its place is a bash-only script that pipes through several filters and genuinely needs the tail of the pipeline to update shell state.
code
bash · 6 lines#!/usr/bin/env bash
shopt -s lastpipe
n=0
printf 'a\nb\nc\n' | while read -r _; do n=$((n+1)); done
echo "$n" # 3 when run as a script; 0 if pasted at an interactive promptgo deeper
Know that lastpipe is a bash option, off by default, that makes the last stage of a pipeline run in the current shell so its variables survive.
Explain the precondition: it applies only when job control is inactive — true for scripts, false at an interactive prompt, where you would need set +m first — and note that only the final stage is affected.
Weigh it against a plain redirect or process substitution, which fix one loop instead of changing every pipeline in the file, and flag that stock macOS bash 3.2 rejects the option outright so a version guard is part of using it.
Decide the standard: a file-wide option that silently changes pipeline semantics is a maintenance hazard for whoever writes the next pipeline, so state when adopting one is worth it, how it must be documented, and what the version floor for your scripts is.
## The default, and what lastpipe changes Bash normally forks a process for every stage of a pipeline. `a | b | c` is three children; the calling shell waits for them and keeps nothing they did to their own variables. That is the mechanism behind the most famous bash surprise — a `while read` loop at the end of a pipe that counts, accumulates or sets a flag, and then hands back nothing. `shopt -s lastpipe` narrows that rule by exactly one stage: the **last** command of the pipeline runs in the current shell process rather than a forked child. Earlier stages still fork — they must, since they run concurrently. So with the option set: ```bash shopt -s lastpipe n=0 printf 'a\nb\nc\n' | while read -r _; do n=$((n+1)); done echo "$n" # 3 ``` And the same option makes a bare `read` at the end of a pipeline actually populate a variable: ```bash shopt -s lastpipe printf 'hello\n' | read -r greeting echo "$greeting" # hello ``` ## The precondition: job control must be inactive This is the part candidates miss, and it is why the option looks broken when people try it. Bash documents lastpipe as taking effect only when **job control is not active**. Job control is the machinery that assigns each pipeline its own process group so it can be suspended and resumed; keeping the last stage in the shell's own process would break that model, so bash simply ignores the option when job control is on. The practical mapping: - **In a script** (a non-interactive shell), job control is off by default, so `shopt -s lastpipe` works as advertised. - **At an interactive prompt**, job control is on by default. Setting lastpipe there appears to do nothing until you also turn job control off with `set +m` — at which point you have disabled a feature you presumably want interactively. So the option is essentially a scripting feature, and testing it by pasting into your terminal will mislead you. ## Version and portability lastpipe was added in **bash 4.2**. Consequences: - `/bin/bash` on stock macOS is 3.2; `shopt -s lastpipe` there fails with an invalid-option error and a non-zero status — which, in a script running under `set -e`, aborts on the very line that was supposed to make it more convenient. - `dash`, `ash` and other POSIX shells have no `shopt` at all, so a `#!/bin/sh` script cannot use it. - If you depend on it, assert the version early rather than letting the failure surface as a mysteriously-zero counter: check `${BASH_VERSINFO[0]}` and exit with a clear message. ## Judging whether to use it The honest comparison is against the alternatives that solve the same problem locally: | Approach | Scope of change | Portability | |---|---|---| | `done < file` | one loop | any POSIX shell | | `done < <(cmd)` | one loop | bash/ksh/zsh | | `mapfile -t arr < <(cmd)` | one read | bash 4+ | | `shopt -s lastpipe` | every pipeline in the file | bash 4.2+, job control off | The first two fix the specific loop and leave the rest of the script's semantics alone. lastpipe changes what *every* pipeline in the file means, including ones written later by someone who did not notice the `shopt` at the top — and the change is invisible at the call site. That is a real maintenance cost for a saving of a few characters. Where it genuinely pays: a bash-only script whose data flows through a chain of filters — `grep | sort | awk | while read` — where restructuring into a redirect would mean either a temp file or wrapping the whole chain in a process substitution that hurts readability. There, one documented `shopt -s lastpipe` near the top, with a version guard, is defensible. ## Second-order effects worth knowing Because the last stage now runs in the calling shell, things that were harmlessly contained in a child are no longer contained: an `exit` in that stage exits the script, a `cd` in it moves the script, and a trap installed there is installed on the script. That is usually exactly what you wanted — it is the point of the option — but it means a pipeline is no longer a sandbox, and a stage written on the assumption that it was one can now change state you did not expect it to touch.
- Why does bash disable lastpipe when job control is active rather than just making it work?Job control puts each pipeline in its own process group so it can be suspended, resumed and reported as one job. If the last stage were the shell itself, the shell would have to be a member of the job's process group, which conflicts with it being the job's controller. Bash sidesteps the conflict by ignoring the option instead of producing a half-working job.
- With lastpipe set, does the earlier part of the pipeline still run concurrently with the last stage?Yes. Only the final stage moves into the calling shell; every earlier stage is still a forked process running at the same time and streaming into the pipe. So a long pipeline is still concurrent, and the shell reads from it as it goes rather than after it completes.
- You inherit a script with `shopt -s lastpipe` at the top. What would you check before changing anything?Which pipelines depend on it — any whose last stage assigns state read afterwards — because removing it silently zeroes them. Also whether anything in a final stage does `exit`, `cd` or installs a trap, since with lastpipe those now affect the whole script. And confirm the shebang pins bash 4.2+, or the option itself fails on older hosts.
saying these in an interview costs you the question
- Says lastpipe makes every pipeline stage run in the current shell
- Expects it to work unchanged at an interactive prompt
- Assumes POSIX sh or dash supports shopt -s lastpipe
- Believes lastpipe is enabled by default in bash 5
- Thinks it removes the fork for the earlier stages too