A SOLUTION:
Add this line on top of your work.sh
script trap exit SIGINT
to have an explicit SIGINT handler:
#! /bin/bash
trap exit SIGINT
COUNTER=0
while true
do
((COUNTER+=1))
echo "#${COUNTER} Working..."
sleep 1
done
Running work
executable now prints:
#1 Working...
#2 Working...
#3 Working...
#4 Working...
#5 Working...
after which it returns back to shell.
THE PROBLEM:
I found this webpage linked in a comment to this question on Unix stackexchange (For the sake of completeness, here also the webpage linked in the accepted answer.) Here's a quote that might explain what's going on:
bash is among a few shells that implement a wait and cooperative exit approach at handling SIGINT/SIGQUIT delivery. When interpreting a script, upon receiving a SIGINT, it doesn't exit straight away but instead waits for the currently running command to return and only exits (by killing itself with SIGINT) if that command was also killed by that SIGINT. The idea is that if your script calls vi for instance, and you press Ctrl+C within vi to cancel an action, that should not be considered as a request to abort the script.
So imagine you're writing a script and that script exits normally upon receiving SIGINT. That means that if that script is invoked from another bash script, Ctrl-C will no longer interrupt that other script.
This kind of problem can be seen with actual commands that do exit normally upon SIGINT by design.
EDIT:
I found another Unix stackexchange answer that explains it even better. If you look at bash(1)
man pages the following is also quite explanatory:
Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.
especially when considering that:
Signals ignored upon entry to the shell cannot be trapped, reset or listed.
Basically, running work.sh
runs it in a separate execution environment:
When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment.
This includes the signal handlers which (if not explicitly present) will ignore SIGINT
and SIGQUIT
by default.