#!/bin/bash
# ----------------------------------------------
# Bash - How to fully exit a program from within
# a function which returns data to the caller.
# ----------------------------------------------
# Program to demonstrate an issue with Bash.
# The problem is: You want to write a function
# which can either:
# - Return data to the caller.
# - Or exit the script completely if it
# discovers some kind of a problem.
# The problem is that Bash can't do both. You can
# only exit the script if you are at the top level
# or you're inside a function where the call to the
# function doesn't try to retrieve data from it.
# You can't do "variable=$( function )" if the
# function wants to maybe exit the script. The
# reason is that when you use the "$( function )"
# syntax, it's actually launching a whole
# separate Bash script process to run the
# function, so "exit 1" only exits that function.
#
# This program demonstrates the problem and the
# work-around.
# ----------------------------------------------
# Work-around is here
# ----------------------------------------------
# These lines are part of a work-around to the
# problem that this program demonstrates. This
# was obtained from the following StackOverflow
# question:
# https://stackoverflow.com/questions/9893667/is-there-a-way-to-write-a-bash-function-which-aborts-the-whole-execution-no-mat
# Instead of relying on "exit 1" to exit the function,
# create trap and trigger the trap from within the
# function. The trap executes the code block within
# the quotes when it recieves a TERM signal. It
# executes this code block from the context of the
# top level of the script, outside the function.
# To accomplish this, we also need to set an
# environment variable based on "$$" which gets
# us the scripts's PID so that we know where to
# send that TERM signal when the time comes.
# To trigger the trap, use the command
# "kill -s TERM $TOP_PID" inside the function
# where you want to exit the program.
trap "echo TOPEXIT >&2 && exit 1" TERM
export TOP_PID=$$
echo "The top level PID is $TOP_PID" >&2
# ----------------------------------------------
# Function: Test for a problem using a parent
# function. If there is no problem, then call a
# child function. Also gather some data and return
# it. If there is any problem, exit the program.
# ----------------------------------------------
TestForParentProblem()
{
# Secondary work-around for a child function.
# When trapping inside a child function,
# you must chain up another signal to the
# from the child, to here, and on to the parent
# level trap in order to get a proper kill.
# If you don't do this, and you send the signal
# from the child all the way to the top level
# (skipping a trap here in this function), then
# this function will still execute to its end,
# hitting instructions you don't want it to hit.
# This secondary work around must send a signal to
# the top-level PID and exit, in this context here
# inside this function, in order to stop the
# execution of the code inside this function.
# Note that $BASHPID gets us this function's PID
# so that the signal coming up from the child can
# go to the correct place here.
trap "echo SUBEXIT >&2 && kill -s TERM $TOP_PID && exit 1" TERM
export PARENT_PID=$BASHPID
echo "The parent function PID is $PARENT_PID" >&2
echo "Testing for a potential parent problem..." >&2
if "$parentProblem" = true
then
echo "There was a parent problem. Exiting program now." >&2
# ----------------------------------------------
# MAIN BUG CAN BE DEMONSTRATED HERE
# ----------------------------------------------
# The goal is to exit the script at this point,
# but "exit 1" does not work as expected here.
# TO DEMONSTRATE THE PROBLEM, UNCOMMENT
# THIS LINE OF THE SCRIPT AND RUN IT.
# exit 1
# Instead, to fix the problem, invoke the special
# trap (created above) to exit the script at the
# main level instead of from within this lower
# level function. Here is the work-around:
echo "Sending kill signal to top level PID $TOP_PID" >&2
kill -s TERM $TOP_PID
exit 1
# Those lines combined with the trap above, works
# around the problem. NOTE: We must still do an Exit 1
# above, to prevent the rest of the function from
# continuing while we wait for the trap to trigger.
# Check for bug.
echo "----------------------------------------------------------------" >&2
echo "BUG PARENT 1: If you can read this, the program did not exit." >&2
echo "----------------------------------------------------------------" >&2
else
echo "No parent problem found." >&2
fi
# Test to see if the workaround succeeds in a child routine.
returnFromSubTest=$( SubTestForProblem )
echo "Returned Data from child function was: $returnFromSubTest" >&2
if "$subProblem" = true
then
echo "----------------------------------------------------------------" >&2
echo "BUG CHILD 2: If you can read this, the program did not exit." >&2
echo "----------------------------------------------------------------" >&2
fi
echo "Returning data from parent function." >&2
returnData="Some_Parent_Data"
echo $returnData
}
# ----------------------------------------------
# Function: Sub-test for a problem and kill the
# program if there is a problem. This will be
# called as a child sub-function from the
# TestForParentProblem function.
# ----------------------------------------------
SubTestForProblem()
{
echo "SubTesting Now (child subroutine called from parent subroutine)..." >&2
if "$subProblem" = true
then
echo "There was a subtest problem. Exiting program." >&2
# ----------------------------------------------
# SECONDARY BUG CAN BE DEMONSTRATED HERE
# ----------------------------------------------
# The goal is to exit the script at this point,
# using the KILL signal workaround, but even that fails
# us here. If the work-around is working as I expect it
# to work, the program should be killed from the child
# function just as effectively as if I had killed it from
# the parent function. However, there is still a problem.
# Because the KILL is just a signal, the parent function
# continues to finish executing if you send a KILL signal
# to it from within the child function. So, for example,
# you cannot do this here and have it work as expected:
# echo "Sending kill signal to top level PID $TOP_PID" >&2
# kill -s TERM $TOP_PID
# exit 1
# The above does not work and the parent function continues
# executing commands even after you sent the KILL. So
# instead, you have to daisy chain the KILLs up from child
# to parent. There is a separate TERM trap defined for the
# sub function in the parent function. Invoke it here to
# work around the problem:
echo "Sending kill signal to parent function PID $PARENT_PID" >&2
kill -s TERM $PARENT_PID
exit 1
# Check for bug.
echo "----------------------------------------------------------------" >&2
echo "BUG CHILD 1: If you can read this, the program did not exit." >&2
echo "----------------------------------------------------------------" >&2
else
echo "No sub problem found." >&2
fi
# Return data from the function.
echo "Returning data from sub function." >&2
subReturnData="Some_Child_Data"
echo $subReturnData
}
# ----------------------------------------------
# Main program code body
# ----------------------------------------------
echo "Starting tests now." >&2
# ----------------------------------------------
# Baseline test - There should be no problem and
# it should return data from both functions.
# ----------------------------------------------
echo "" >&2
echo "Baseline test - No problems in either parent function or child function." >&2
parentProblem=false
subProblem=false
returnedData=$( TestForParentProblem )
echo "Returned Data from parent function was: $returnedData" >&2
# ----------------------------------------------
# Test child routine - Try exiting during
# the child subroutine. It should stop executing
# code from both the child function and its
# parent function.
#
# NOTE: COMMENT THIS SECTION OUT IF YOU WANT TO
# TEST THE PARENT TEST BELOW.
# ----------------------------------------------
echo "" >&2
echo "Child subroutine test - Problem in child function but not in parent function." >&2
parentProblem=false
subProblem=true
returnedData=$( TestForParentProblem )
echo "Returned Data from parent function was: $returnedData" >&2
echo "----------------------------------------------------------------" >&2
echo "BUG MAIN 1: If you can read this, the program did not exit." >&2
echo "----------------------------------------------------------------" >&2
# ----------------------------------------------
# Parent test - There should be a parent problem
# encountered and it should quit the program
# without trying to display any data at all.
#
# NOTE: COMMENT OUT THE CHILD TEST ABOVE IF YOU
# WANT TO RUN THIS TEST.
# ----------------------------------------------
echo "" >&2
echo "Parent test - Problem in parent function but not in sub function." >&2
parentProblem=true
subProblem=false
returnedData=$( TestForParentProblem )
echo "Returned Data from parent function was: $returnedData" >&2
echo "----------------------------------------------------------------" >&2
echo "BUG MAIN 2: If you can read this, the program did not exit." >&2
echo "----------------------------------------------------------------" >&2