Signals cause wait to exit with a status equal to the signal number plus 128. To get the real exit status, loop until $? is less.

#!/bin/bash

trap 'echo trapped' INT TERM
sleep 10 & pid=$!

while wait $pid; test $? -ge 128
do echo 'finished wait'
done

echo bye

Note that test overwrites $? with its own exit status, so if you care about the real exit status from wait, you'll have to create a variable between wait and test.

Answer from Potatoswatter on Stack Overflow
🌐
The Linux Documentation Project
tldp.org › LDP › Bash-Beginners-Guide › html › sect_12_02.html
12.2. Traps
When Bash receives a signal for which a trap has been set while waiting for a command to complete, the trap will not be executed until the command completes.
Discussions

bash - How can I kill and wait for background processes to finish in a shell script when I Ctrl+C it? - Unix & Linux Stack Exchange
Any ideas on how I can make this shell script wait for the children to die after sending INT? #!/bin/bash trap 'killall' INT killall() { echo "**** Shutting down... ****" kill 0 -INT wait # Why doesn't this wait?? More on unix.stackexchange.com
🌐 unix.stackexchange.com
shell - Interrupt sleep in bash with a signal trap - Stack Overflow
The signal trap works, but the message being echoed is not displayed until the sleep 10 has finished. It appears the bash signal handling waits until the current command finished before processing the signal. More on stackoverflow.com
🌐 stackoverflow.com
bash - Wait for signal - Unix & Linux Stack Exchange
In a bash script, is there a simple way to wait for a signal, something like: wait -s SIGINT or whatever? Maybe just trap? More on unix.stackexchange.com
🌐 unix.stackexchange.com
How to trap and exit cleanly?
When aws runs in the foreground, it is the process that receives SIGINT when you press Ctrl+C. I'm not familiar with the aws command, but the problem could be that it is not well-behaved and upon termination does not appropriately communicate the fact that it was interrupted by SIGINT back to the parent process, i.e. bash. As a workaround, you can try running aws in the background: find . -maxdepth 1 -printf "%P\n" | egrep -v '^$' | while read -r line ; do aws s3 sync "$line" "s3://$bucket/$line" & wait "$!" # wait for aws to finish done This way, bash remains the foreground process, so it will be the process receiving and handling SIGINT when you press Ctrl+C. More on reddit.com
🌐 r/bash
15
11
February 25, 2020
🌐
PhoenixNAP
phoenixnap.com › home › kb › sysadmin › bash trap command explained
Bash trap Command Explained
December 19, 2025 - The while loop in the example above executes infinitely. The first line of the script contains the trap command and the instructions to wait for the SIGINT signal, then print the message and exit the script.
🌐
Linux Journal
linuxjournal.com › content › bash-trap-command
The Bash Trap Command | Linux Journal
August 7, 2019 - The problem now is because the loop is taking input from a pipe. The original bash process has now executed one sub-process for "journalctl" and another sub-process for "while read line ...". When bash executes a command, per the man page: traps caught by the shell are reset to the values inherited from the shell's parent
Top answer
1 of 2
2

Can anybody tell why there the difference is?

Ctrl+C gets to sleep as well, while kill <pid> doesn't. In effect the latter way needs sleep to exit by itself before hi! is printed. What you call "1 second delay" is about 1 second at most and about 0.5 on average (it may take longer if the OS is very busy with other tasks).

You can clearly see the difference by using

pv -qL 1 <<< "0123456789"

instead of sleep 1.

Note that

kill -- -<pid>    # note the minus sign before PID

would send the signal to the entire process group, so to sleep (or pv) as well.


Is there a way that always trigger trap command immediately no matter what is going on?

There is this question How to handle signals in bash during synchronous execution?

From one of the answers:

If bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes. When bash is waiting for an asynchronous command via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status greater than 128, immediately after which the trap is executed.

So, run your external program asynchronously and use wait. Kill it using $!.

In your case this method leads to the following quick and dirty script:

#!/bin/bash

trap 'kill "$!"; echo hi!' SIGINT SIGTERM

echo "pid is $$"
while true; do
    sleep 1 &
    wait
done
2 of 2
0

Instead of sleep 1 try sleep 1 <&0 & wait and then you'll find it quits right away.

The sleep command will remain running, however.

This works because bash's wait command can be interrupted by a signal, but the internal wait can't be.

Of course <&0 isn't required for sleep, but for other commands that are being run in the background this way, stdin would be disconnected unless we deliberately connect it in this way.

🌐
SS64
ss64.com › bash › trap.html
trap Man Page - Linux
When Bash receives a signal for which a trap has been set while waiting for a command to complete, the trap will not be executed until the command completes.
🌐
OneUptime
oneuptime.com › home › blog › how to handle signal trapping in bash
How to Handle Signal Trapping in Bash
January 24, 2026 - #!/bin/bash # Test script to verify signal handlers work correctly test_signal_handler() { local script="$1" local signal="$2" local expected="$3" echo "Testing $signal on $script..." local output_file output_file=$(mktemp) # Start script in background bash "$script" >"$output_file" 2>&1 & local pid=$! sleep 1 # Send signal kill -"$signal" "$pid" sleep 1 # Check result if wait $pid 2>/dev/null; then echo " Script exited cleanly" else local exit_code=$? echo " Script exited with code: $exit_code" fi if grep -q "$expected" "$output_file"; then echo " Expected output found: $expected" else echo " Expected output missing: $expected" fi rm -f "$output_file" } # Create test script cat > /tmp/test_script.sh << 'EOF' #!/bin/bash trap 'echo "Cleanup!"; exit 0' SIGTERM while true; do sleep 1; done EOF chmod +x /tmp/test_script.sh test_signal_handler /tmp/test_script.sh TERM "Cleanup!"
Top answer
1 of 5
23

Your kill command is backwards.

Like many UNIX commands, options that start with a minus must come first, before other arguments.

If you write

kill -INT 0

it sees the -INT as an option, and sends SIGINT to 0 (0 is a special number meaning all processes in the current process group).

But if you write

kill 0 -INT

it sees the 0, decides there's no more options, so uses SIGTERM by default. And sends that to the current process group, the same as if you did

kill -TERM 0 -INT    

(it would also try sending SIGTERM to -INT, which would cause a syntax error, but it sends SIGTERM to 0 first, and never gets that far.)

So your main script is getting a SIGTERM before it gets to run the wait and echo DONE.

Add

trap 'echo got SIGTERM' TERM

at the top, just after

trap 'killall' INT

and run it again to prove this.

As Stephane Chazelas points out, your backgrounded children (process1, etc.) will ignore SIGINT by default.

In any case, I think sending SIGTERM would make more sense.

Finally, I'm not sure whether kill -process group is guaranteed to go to the children first. Ignoring signals while shutting down might be a good idea.

So try this:

#!/bin/bash
trap 'killall' INT

killall() {
    trap '' INT TERM     # ignore INT and TERM while shutting down
    echo "**** Shutting down... ****"     # added double quotes
    kill -TERM 0         # fixed order, send TERM not INT
    wait
    echo DONE
}

./process1 &
./process2 &
./process3 &

cat # wait forever
2 of 5
11

Unfortunately, commands started in background are set by the shell to ignore SIGINT, and worse, they can't un-ignore it with trap. Otherwise, all you'd have to do is

(trap - INT; exec process1) &
(trap - INT; exec process2) &
trap '' INT
wait

Because process1 and process2 would get the SIGINT when you press Ctrl-C since they're part of the same process group which is the foreground process group of the terminal.

The code above will work with pdksh and zsh which in that regard are not POSIX conformant.

With other shells, you would have to use something else to restore the default handler for SIGINT like:

perl -e '$SIG{INT}=DEFAULT; exec "process1"' &

or use a different signal like SIGTERM.

Find elsewhere
🌐
TutorialsPoint
tutorialspoint.com › article › bash-wait-command-with-examples
Bash wait Command with Examples
April 12, 2023 - We can also use wait command with trap command to handle signals and terminate background processes when necessary. For example, suppose we have a script that starts a background process and we want to terminate it if user presses Ctrl+C ? ...
🌐
Wp-p
wp-p.info › home › bash dictionary › wait / trap
wait / trap | Bash Dictionary - ウェブプログラミングプレイス
March 13, 2026 - wait # Wait for all background jobs to finish wait $PID # Wait for the specified PID to finish wait %job_num # Wait for the specified job to finish · Use trap to catch signals.
Top answer
1 of 1
1

This other answer explains why you see trap worked only after done. If you run the subshell in the background and then wait for it, the result will be different:

#!/bin/bash

trap 'echo "trap worked"' INT

(
   trap '' INT
   sleep 5
   echo "done"
) &

wait

If you hit Ctrl+c when the subshell works, it will trigger the trap immediately. It will also make the script go past wait and thus finish. Run wait in a loop if you don't want the script to finish:

until wait; do :; done

This way you will be able to trigger the trap multiple times easily and the script will exit after the subshell exits.

I guess this is not exactly what you mean by "without interrupting the foreground task", since the task (here: sleep) is in the background instead and it has its consequences. When job control is disabled (and it is by default in a script), & redirects stdin to /dev/null or equivalent file. This way what you run in the background cannot steal input. If you want our background subshell to be able to read from the stdin of the whole script then you need to explicitly redirect stdin to stdin (<&0). It looks like a no-op, but with & it actually makes sense, it makes an asynchronous job behave more like a job in the foreground:

(…) <&0 &

Another thing: note wait without arguments waits for all currently active child processes. If your actual script runs more jobs in the background and you want wait not to wait for some of them, the easiest way may be to disown.


Example code:

#!/bin/bash

trap 'echo "trap worked"' INT

(
   trap '' INT
   sleep 10
   echo "(not waited for) done"
) &
disown "$!"

(
   trap '' INT
   cat
   echo "(waited for) done"
) <&0 &

until wait; do :; done

Notes:

  • cat and sleep are both immune to Ctrl+c.
  • You can strike Ctrl+c and trigger the trap multiple times.
  • At the same time you can write lines to cat and it will work, i.e. it will print upon Enter (but if you Ctrl+c in the middle of a line then what you typed so far (i.e. what hasn't been delivered to cat yet) will be discarded and never delivered to cat because this is how the line discipline works).
  • Exit cat with Ctrl+d before sleep 10 finishes and you will see the script does not wait for the disowned job.

Side note: while playing with SIGINT and bash, it's good to know bash implements a "wait and cooperative exit" approach at handling SIGINT/SIGQUIT delivery. See this link and this article.

🌐
SS64
ss64.com › mac › trap.html
TRAP Command: Execute a command when the shell receives a signal in macOS
When Bash is waiting for an asynchronous command via the wait built-in, the reception of a signal for which a trap has been set will cause the wait built-in to return immediately with an exit status greater than 128, immediately after which the trap is executed.
Top answer
1 of 2
10

No.

wait is exclusively used in a parent process to wait for the termination of a child process (and to access its exit status).

Furthermore, no process may trap the KILL signal (the original question used KILL as example).

Also, to "wait for a signal" is an unusual thing to want to do, as signals are asynchronous events, meaning you don't wait for them, but instead you install a signal handler (using trap in the shell) that would handle the signal whenever it arrives. A signal could be arriving at any time during the script's execution, and the signal handler would be executed when that happens (the normal program flow would temporarily pause while the signal is being handled).

You could, obviously, do something like

trap 'quit=1' USR1

quit=0
while [ "$quit" -ne 1 ]; do
    printf 'Do "kill -USR1 %d" to exit this loop after the sleep\n' "$$"
    sleep 1
done

echo The USR1 signal has now been caught and handled

to do a sort of a "waiting for signal to arrive" loop.

Here, the "trap" would "catch" the USR1 signal, the "handler" would set quit to 1, and control would be returned to the code, which would exit the loop.

2 of 2
3

Without loop and second process

trap "echo >&3" SIGINT
exec 3<> <(:)
read <&3

This creates pipe fd 3, opened both for read and write by starting subshell with command ":" (alias for "true"). And then blocks at reading from it. Signal handler unblocks read by sending empty line into the pipe. The same could be achieved using named pipe (fifo).

🌐
Nick Janetakis
nickjanetakis.com › blog › using-trap-to-run-a-command-after-your-shell-script-exits
Using trap to Run a Command after Your Shell Script Exits — Nick Janetakis
May 16, 2023 - I didn’t demo it in the script ... "whoami && echo 'COOL STORY'" EXIT. If you’re running bash you can run trap -l to get a full list of signals....
🌐
Reddit
reddit.com › r/bash › how to trap and exit cleanly?
r/bash on Reddit: How to trap and exit cleanly?
February 25, 2020 -

Code:

find . -maxdepth 1 -printf "%P\n" | egrep -v ^$ | while read -r line ; do

  aws s3 sync  $line s3://$bucket/$line

done

I'd like to add a trap to exit cleanly , but everything I've tried kills the current running aws s3 sync then moves to the next read line and continues on. I know it's probably something small I am overlooking, how do I get the script exit out on ctrl + c correctly ?

🌐
LinuxCommand.org
linuxcommand.org › lc3_wss0150.php
Writing shell scripts - Lesson 15: Errors and Signals and Traps (Oh, My!) - Part 2
#!/bin/bash echo "this script will endlessly loop until you stop it" while true; do : # Do nothing done · After we launch this script it will appear to hang. Actually, like most programs that appear to hang, it is really stuck inside a loop. In this case, it is waiting for the true command to return a non-zero exit status, which it never does.
🌐
Lgfang
lgfang.github.io › computer › 2020 › 02 › 13 › bash-signal-trap
More on Signals in Bash
In fact, if you review the test.out ... is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes....