Exit shell script from a subshell
Recently, I was writing a bash script that had many fatal error conditions where I wanted to output a message to stderr then kill the script with an error code. The problem was some of the commands were in subshells, making it impossible to create a clean way to accomplish my goal.
What are subshells
When a command is executed in shell/bash inside a set of parenthesis it spawns a new process which doesn’t have access to quit the parent script.
Example script:
#!/bin/sh
echo "output before exit"
# This subshell will not cause the parent process to exit
(exit 1)
echo "output after exit"
Result of running the script:
output before exit
output after exit
Use signals to exit process from subshell
Before going any further, here is an example script that allows the subshell to terminate the process:
#!/bin/sh
trap "exit 1" 10
PROC="$$"
fatal(){
echo "$@" >&2
kill -10 $PROC
}
echo "output before exit"
(fatal "no more output, script exiting")
echo "output after exit"
Result of running the script:
output before exit
no more output, script exiting
What are signals
Processes in *nix based operating systems can respond to signals to perform an action from outside the process. This is the mechanism that allows you to exit a command line process by pressing ctrl-c. When you press ctrl-c in your terminal, an interrupt signal (SIGINT) is sent to the current process. The process then responds to that signal by exiting (if it’s a well behaved process).
You can see all the signals defined on your system by running trap -l
, which will give you output like the following:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Next to each signal (such as SIGINT) is the number (2 in the case of SIGINT). The script above that allowed the subshell to exit utilized “10”, the SIGUSR1 signal.
How to respond to a signal using trap
A shell script can listen for a signal by using trap
. To demonstrate how trap
works we’ll create a script that responds to the SIGINT signal and echos a message:
#!/bin/sh
# 2 is the SIGINT signal
trap 'echo "received SIGINT"' 2
# This could also be written as:
# trap 'echo "received SIGINT"' INT
# Note, SIGINT will be sent to child jobs, so the sleep
# process will be interrupted when SIGINT is received.
sleep 30
echo "echo after sleep finishes"
Running this script then pressing ctrl-c will result in the following:
^Creceived SIGINT
echo after sleep finishes
See trap --help
for additional usage.
Putting it together to exit a script from a subshell
In a script, we’ll first create a trap for SIGUSR1 (a user defined signal) to act upon that will exit the process with exit code 1:
trap "exit 1" 10
Then, we’ll store the process ID of the script being executed so that we can send a signal to it:
PROC=$$
Lastly, we’ll define a function that the process and subshell processes can invoke that will echo a message to stderr and send our SIGUSR1 signal to the script’s process using kill
:
fatal(){
echo "$@" >&2
kill -10 $PROC
}
What we end up with is the following:
#!/bin/sh
# fatal uses SIGUSR1 to allow clean fatal errors
trap "exit 1" 10
PROC=$$
fatal(){
echo "$@" >&2
kill -10 $PROC
}
Example
I’ve saved the following as test.sh and made it executable with chmod +x test.sh
:
#!/bin/sh
# fatal uses SIGUSR1 to allow clean fatal errors
trap "exit 1" 10
PROC=$$
fatal(){
echo "$@" >&2
kill -10 $PROC
}
test -n "$1" || fatal "you must provide a path to a file as the first argument"
test -f $1 || fatal "'${1}' isn't a file"
(fatal "'${1}' pointed to a file, good job, but we're proving the subshell fatals too")
echo "This should never output"
When executing the script:
-
./test.sh
gives:you must provide a path to a file as the first argument
-
./test.sh .
gives:'.' isn't a file
-
./test.sh ./test.sh
gives:'test.sh' pointed to a file, good job, but we're proving the subshell fatals too
All exit with exit code 1.