How to Make a Bash Script Self‑Terminate Cleanly with trap and kill

This article explains why background loops in Bash scripts can survive script termination, demonstrates the problem with concrete examples, and provides robust solutions using trap, killall/pkill, and kill 0 to ensure all child processes exit together.

Liangxu Linux
Liangxu Linux
Liangxu Linux
How to Make a Bash Script Self‑Terminate Cleanly with trap and kill

Problem description

When a Bash script starts a background command (e.g., sleep 50 &) and the script ends or is interrupted (e.g., Ctrl+C), the background process may become a child of init / systemd instead of terminating with the script. This is especially noticeable when the background command is a loop built from Bash keywords such as while, for, until, if, or case.

Illustrative scripts

Simple script that launches a single background sleep and kills it via $!:

# cat test1.sh
#!/bin/bash
echo $BASHPID
sleep 50 &
kill $!

When the background command is a loop, a second Bash process is created to provide the execution environment for the loop:

# cat test2.sh
#!/bin/bash
echo $BASHPID
while true; do
  sleep 50
  echo 1
done &
sleep 60

Running pstree -p | grep test2.sh shows two Bash processes: the original script and a subshell that runs the while loop. Killing the parent script leaves the subshell alive, which then re‑parents to init.

Why Bash built‑ins create extra shells

Bash keywords ( while, for, until, if, case) are not external commands; they run inside the current Bash process. When they are placed in the background, Bash must fork a new Bash subprocess to host the loop and its children. If the backgrounded construct originates from a script, the script process itself becomes the parent of that helper Bash. Consequently, terminating the original script does not automatically terminate the helper Bash, which then becomes an orphan and is adopted by init.

Basic kill attempts

Using kill $! works for a single background command, but does not affect the helper Bash created for a background loop. Adding killall $(basename $0) before exit can kill both script processes, yet it fails when the script is invoked as bash test1.sh because the process name becomes bash instead of the script filename.

Robust solution with pkill

Replace killall with a pattern‑based pkill and also kill the explicit background job stored in $!:

# cat robust.sh
#!/bin/bash
# Terminate on common signals and on script errors
trap 'pkill -f $(basename $0); exit 1' SIGINT SIGTERM EXIT ERR

while true; do
  sleep 1
  echo "hello world!"
done &
# Additional foreground work
sleep 60

This trap runs whenever the script receives SIGINT, SIGTERM, exits normally, or encounters an error, ensuring that both the helper Bash (matched by its filename) and the explicit background job are killed.

Group termination with kill 0

The kill manual states that a PID of 0 sends the signal to every process in the current process group. A trap that executes kill 0 therefore terminates all processes spawned by the script, regardless of how they were started:

# cat kill0.sh
#!/bin/bash
trap 'echo signal_handled:; kill 0' SIGINT SIGTERM
while true; do
  sleep 5
  echo "hello world! hello world!"
done &
sleep 60

Summary of recommended patterns

For scripts that may be interrupted, install a trap handling SIGINT, SIGTERM, EXIT, and ERR.

Inside the trap, either:

Use pkill -f $(basename $0) to kill any helper Bash processes that carry the script name, and optionally kill $! to terminate the most recent background job.

Or use kill 0 to send a signal to the entire process group.

Prefer pkill -f when the script may be invoked as ./script.sh or bash script.sh, because the process name differs in the two cases.

When only a single background command is needed, storing its PID in a variable (e.g., pid=$!) and killing it explicitly is sufficient.

Key take‑aways

Backgrounding Bash built‑ins spawns a helper Bash subprocess. If the parent script exits without explicitly terminating that helper, the helper (and its children) become orphans under init. Using a comprehensive trap that either pkill -f $(basename $0) or kill 0 guarantees that all child processes are cleaned up, making the script truly self‑destructing.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

processShellBashscripttrap
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.