Bash / Intermediate: reliable data and failure paths / Lesson 8
Choose the correct execution boundary
Explain state isolation, trailing-newline removal, pipeline environments, and process substitution before selecting a data-return mechanism.
On this page
Lesson goals
- Predict which assignments survive a child environment.
- Distinguish command substitution from process substitution.
- Keep input loops in the intended shell without hiding producer failure.
A subshell copies state without returning mutations
Parenthesized groups, ordinary command substitutions, asynchronous commands, and usually pipeline stages run in subshell environments. They start from relevant inherited state but cannot rewrite the parent’s variables or current directory. This is useful isolation, not a bug to defeat blindly. A function is normally current-shell work until its invocation is placed in a child-producing context. Track the context around a helper rather than assuming its body determines persistence. Bash’s lastpipe option can alter the final pipeline stage under documented conditions, so scripts should not rely on an undeclared interactive option setting.
Command substitution is a text interface
$(command) captures standard output and removes trailing newline characters. Embedded newlines remain, but an unquoted use of the result can split and glob them later. Ordinary command substitution also isolates state changes. Newer Bash documentation describes additional substitution forms that may execute in the current environment; those are not assumed available in this course’s tested Bash baseline. Use the well-understood ordinary form when portability to that baseline matters. Separate status checking from declarations and do not use text capture as a binary or arbitrary-record container.
value=outer
captured=$(value=inner; printf 'first\nsecond\n\n')
printf 'state=%s length=%s\n' "$value" "${#captured}"
printf '<%s>\n' "$captured"
state=outer length=12
<first
second>
The captured text contains one embedded newline and no trailing newline. The assignment inside the substitution leaves the parent value unchanged.
A pipe-fed loop may update only a child
A common loop reads lines from a producer pipeline and increments a counter, then finds the counter unchanged afterward. The loop’s child environment explains the result. Put the loop’s input on a redirection when it must update the current shell, or deliberately return an aggregate through a documented text interface. Do not move code until the state requirement is clear. Pipeline stage exit status and variable persistence are separate questions: a loop can succeed while its changes vanish, and a persistent loop can still have an unobserved producer failure.
Process substitution supplies a path to asynchronous work
Bash <(list) and >(list) expose a producer or consumer through a filename-like interface when the platform supports the required mechanism. In while ... done < <(producer), the loop can remain in the current shell while the producer runs separately. This is a Bash extension, not a universal POSIX sh feature. The outer command’s success does not automatically establish the substituted process’s success. Design explicit coordination or use a separate checked stage when producer failure matters. Also bound resource lifetime so a background substitution does not outlive the operation unexpectedly.
count=0
while IFS= read -r line; do
count=$((count + 1))
done < <(printf '%s\n' alpha beta)
printf 'count=%s\n' "$count"
count=2
The fixed printf producer is harmless and emits two complete lines. The loop’s counter persists; a real fallible producer would need its own documented status handling.
Choose data flow and status flow together
If the parent needs a count, a small captured result may be enough. If it needs to update several local structures, redirecting a verified input file into the current-shell loop may be clearer. If a streaming producer can fail, decide how to observe that completion before accepting the result. Avoid undocumented lastpipe dependencies and clever current-environment substitutions unavailable on the deployed version. Test with an empty input, a producer that emits partial data then fails, and state that must survive. The best structure makes both isolation and completion visible to a maintainer.
Common errors and fixes
A pipeline loop loses its counter
- Why it happens
- The mutation occurred in a subshell environment.
- How to fix it
- Choose current-shell input redirection or an explicit return.
- Prevent it next time
- Test state after the loop, not only inside it.
Captured text loses final newlines
- Why it happens
- Ordinary command substitution trims them by design.
- How to fix it
- Use a stream or a record-aware representation when needed.
- Prevent it next time
- Document text-capture limitations.
Process substitution hides producer failure
- Why it happens
- Only the consuming loop’s status was checked.
- How to fix it
- Coordinate producer completion or use a checked stage.
- Prevent it next time
- Design failure observation with the stream.
Check your understanding
1. Why does changing a variable inside $(...) not normally update its parent?
Show worked solution
The ordinary substitution executes in a subshell environment. Its captured output returns as text, but assignments do not cross back. Choose a deliberate current-shell interface if mutation is required instead of expecting output capture to transfer state.
2. A loop using < <(producer) counts records correctly. What failure question remains?
Show worked solution
Whether producer completed successfully. It may emit partial records and fail while the loop ends normally. The application must observe or separately validate producer completion before treating the count as complete.
Key takeaways
- State persistence follows execution context.
- Text and process substitution have different data and status contracts.