Course outline

Bash / Advanced: channels, concurrency, and maintainable CLI design / Lesson 14

Coordinate owned jobs and signal outcomes

Separate launch success from completion, track child identities, and apply bounded signal handling only to processes the script owns.

About 4 min readContent reviewed
On this page

Lesson goals

  • Capture $! and wait for each required child.
  • Distinguish interactive job control from script coordination.
  • Interpret signal and wait outcomes without guessed PIDs.

An asynchronous launch is not completed work

Appending & starts work asynchronously and lets the parent continue. The immediate launch status does not represent the eventual result of the job. Capture $! promptly and retain the association between a child and its intended task. wait retrieves completion for owned children and can return a nonzero result that the parent must handle. A script that exits after launching required work has not established completion. If output from several children is important, give each an explicit destination or aggregation protocol instead of assuming concurrent writes produce a stable order.

Interactive job names are not a script API

Interactive Bash can manage foreground and background process groups with jobs, fg, bg, and terminal-generated signals. A stopped job is not finished, and a shell job can contain several processes in a pipeline. Noninteractive scripts should not depend on interactive job numbering or a particular controlling terminal. Use explicitly captured child identities and documented wait behavior. Process IDs can be reused after lifetimes end, so do not copy a number from an old listing and signal it later. A name match can also include unrelated processes and is unsuitable for precise ownership.

bash Collect two owned children’s completion statuses
( exit 0 ) &
first=$!
( exit 4 ) &
second=$!
wait "$first"
first_status=$?
wait "$second"
second_status=$?
printf 'first=%s second=%s\n' "$first_status" "$second_status"
Expected output
first=0 second=4

Only the two disposable children created here are waited for. The second deliberately returns four; the parent records both results and handles the demonstration successfully.

Choose wait variants against the supported version

Modern Bash supports variants such as wait -n for a next completion and -p for identifying which completion supplied the result under documented conditions. These flags are not assumed on every historical Bash or POSIX sh. A portable-to-older-Bash coordinator can wait for recorded PIDs individually or use a simpler batch barrier. A wait interrupted by a trapped signal needs its own handling and may not mean the child finished. Record supported features and test cancellation paths, no-child conditions, and multiple failures rather than only a successful pair of jobs.

Signals request documented behavior

kill sends a signal; it does not always mean immediate forced termination. A program can handle an appropriate signal and perform its own action, while SIGKILL and SIGSTOP cannot be handled. Use symbolic names instead of assuming signal numbers are identical across platforms. Cancellation of a script’s child work must define forwarding, bounded waiting, cleanup, and the final parent result. The safe example sends a user signal only to a disposable child’s own shell, never a host service or unrelated PID. Do not use broad process-killing recipes as a substitute for ownership tracking.

bash Handle a signal inside a disposable child
bash -c 'trap "echo handled-USR1" USR1; kill -s USR1 "$$"; echo continued'
Expected output
handled-USR1
continued

The inner Bash targets its own PID and continues after the handler. No host process is selected, and the parent does not guess or reuse another process identifier.

Define the aggregate result before launching

Decide whether every child must succeed, whether one failure cancels remaining work, and how partial results are treated. Collect all required statuses even if one is nonzero, so children do not remain unobserved. Preserve the primary failure while separately reporting cleanup or cancellation trouble. A durable workflow may need a service manager or a stronger task system rather than ad hoc shell job control. Bash orchestration remains maintainable when ownership, result aggregation, and shutdown behavior fit in a clear bounded design and can be tested entirely with synthetic children.

Common errors and fixes

Launch success is reported as task success

Why it happens
The parent never waited for the eventual result.
How to fix it
Capture the child and handle wait status.
Prevent it next time
Separate started and completed events in logs.

A stopped job is treated as finished

Why it happens
Terminal suspension was confused with termination.
How to fix it
Inspect state and choose an owned lifecycle action.
Prevent it next time
Test stop, exit, and cancellation distinctly.

A stale PID is signaled

Why it happens
Process lifetime and ownership were not retained.
How to fix it
Operate only on explicitly owned current children.
Prevent it next time
Capture identity at launch and avoid broad matching.

Check your understanding

1. Two children fail with different statuses. What should an aggregate coordinator preserve?

Show worked solution

Preserve both task identities and statuses, then apply a documented aggregate policy such as any-required-failure. Do not let a final successful printf replace the failure result reported to the caller. Cleanup outcomes should remain distinguishable from task outcomes.

2. Why can an interrupted wait require another lifecycle decision?

Show worked solution

A trapped signal can cause wait to return without establishing the child’s final completion. The coordinator must know whether to continue waiting, request cancellation of its owned child, or return under its documented policy.

Key takeaways

  • Track owned children and collect their real completion.
  • Make cancellation, aggregation, and version support explicit.

Official references