Glam Prestige Journal

Bright entertainment trends with youth appeal.

I'm trying to write a script wherein if someone pressed CTRL+C (Keyboard interruption) then it shouldn't exit the entire script, just the current execution function.

Can anyone suggest what logic can I use here? Here is some sample code for the same

#!/bin/bash
func1()
{
wget
cat text1.txt |awk '{print $3}'|tr -d '.' > temp1.txt
}
func2()
{
cat temp1.txt|<stdin-to-some-tool> > temp2.txt
}
func3()
{
<some-tool> temp2.txt > temp3.txt
}

Likewise, other functions should still execute in case we stop the function before that.

1

1 Answer

Analysis

When you hit Ctrl+c, the line discipline of your terminal sends SIGINT to processes in the foreground process group.

Bash, when job control is disabled, runs everything in the same process group as the bash process itself. Job control is disabled by default when Bash interprets a script. This means if a script runs in the foreground then the shell interpreting the script along with everything invoked by the script will belong to the foreground process group1 and will receive SIGINT upon Ctrl+c.

You can enable job control on demand by set -m. When job control is enabled, Bash runs commands in separate process groups and Ctrl+c gets to the group being the foreground process group at the moment.

This leads to two basic approaches in solving your problem.


Solution with traps

This solution works without job control. Job control is disabled by default when Bash interprets a script, so no explicit configuration of the shell is required.

A process may ignore SIGINT. If it does then its children will ignore SIGINT as well2. Even if the shell interpreting the script does not ignore SIGINT, it may make some of its children ignore it3.

You want the main shell to ignore SIGINT. You want it to do this:

trap '' INT

At the same time you want your functions not to ignore SIGINT. You want them to do this:

trap - INT

To do this properly you need to use subshells. A function defined with foo () { … } and executed as foo runs as a part of the main shell, so trap … INT inside the function will affect the main shell. Such function can be run in a subshell on demand (e.g. as (foo) or in a pipeline) and then its trap will not affect the main shell. A function defined with foo () ( … ) will always run in a subshell. For what you want to do it's good to define functions this way4.

Proof of concept:

#!/bin/bash
trap '' INT
foo () ( trap - INT echo 'foo starts' sleep 5 echo 'foo returns'
)
foo
echo 'main shell here'
foo
echo 'main shell exits'

Ctrl+c will interrupt sleep and the function (you won't see foo returns), but not the main shell.

Note any command you directly run from the main part of the script (i.e. not from the function) will inherit its setting to ignore SIGINT. Tu run a command that doesn't ignore it, you don't necessarily need a function, a subshell is enough. Example:

#!/bin/bash
trap '' INT
sleep 5 # immune to ctrl+c
echo 'try now'
(trap - INT; exec sleep 5) # susceptible to ctrl+c

Similarly you can run a command immune to Ctrl+c from inside a function susceptible to Ctrl+c in a script immune to Ctrl+c. It's all about what trap you invoke in what (sub)shell.


Solution with job control

The above approach with traps is somewhat cumbersome. An alternative is to enable job control in the script (set -m). Bash will run commands in separate process groups and Ctrl+c will get to the group being the foreground process group at the moment.

#!/bin/bash
set -m
foo () ( echo 'foo starts' sleep 5 echo 'foo returns'
)
foo
echo 'main shell here'
foo
echo 'main shell exits'

Now when foo runs, it gets its own process group which becomes the foreground process group. Upon Ctrl+c sleep (or whatever runs inside foo) and the subshell itself get SIGINT. The signal can interrupt sleep and the subshell (so you don't see foo returns) but not the main shell because the main shell does not get the signal at all.

Notes:

  • If you happen to hit Ctrl+c when the process group of the main shell is the foreground process group (e.g between two invocations of foo) then the main shell will get SIGINT.
  • If the function was defined as foo () { … } then foo (when running) would get neither its own subshell nor its own process group; sleep however would get its own process group and if Ctrl+c interrupted sleep then you would see foo returns.
  • This answer does not elaborate on all aspects of job control. Read man 1 bash and/or the online documentation and decide if you want to enable job control in your script.

Final remark

The two approaches do not exclude each other. There's nothing wrong in using traps along with job control to achieve the desired behavior, if only one understands how things work (as opposed to voodoo).


Footnotes

1 Except when some process deliberately leaves the group.

2 Except when some child process deliberately chooses not to ignore.

3 E.g. asynchronous processes started in a script ignore SIGINT. This way they don't react to Ctrl+c despite being in the foreground process group. For comparison: in an interactive Bash (where job control is enabled by default) anything started in the background (i.e. with terminating &) does not react to Ctrl+c because it doesn't get SIGINT in the first place. It doesn't get SIGINT because it's in a separate process group which is not the foreground process group. But when you fg it, its group becomes the foreground process group. When in the foreground, it can react to Ctrl+c.

4 However if you want the function to affect the main shell in other aspects (e.g. to modify variables of the main shell) then it must not run in a subshell. The requirements may turn out to be contradictory.

1

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy