#!/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 from inside a function where the call to the
# function doesn't try to retrieve data from it.
# You can't do variable=$(function) where the
# function wants to maybe exit the script.
# This program demonstrates the problem and the
# work-around.
# ----------------------------------------------
# Work-around is here
# ----------------------------------------------
# These two 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
trap "exit 1" TERM
export TOP_PID=$$
# Combine it with "kill -s TERM $TOP_PID" inside
# the function where you want to exit the program.
# ----------------------------------------------
# Function: Test for a problem. If there is no
# problem, then gather some data and return it.
# If there is a problem, exit the program.
# ----------------------------------------------
TestForBadProblem()
{
trap "kill -s TERM $TOP_PID" TERM
export SUB_PID=$$
echo "Testing for a potential bad problem..." >&2
if "$badProblem" = true
then
echo "There was a bad problem. Exiting program now." >&2
# ----------------------------------------------
# BUG IS DEMONSTRATED HERE
# ----------------------------------------------
# The goal is to exit the script at this point,
# but in this spot it will not work as expected
# in every case, it will not always exit the
# script. 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:
kill -s TERM $TOP_PID
# That line combined with the trap above, works
# around the problem.
else
echo "No bad problem found." >&2
fi
# New test: Additional test to see if the workaround
# succeeds in a sub-sub routine.
returnFromSubTest=$( SubTestForProblem )
if "$subProblem" = true
then
echo "----------------------------------------------------------------" >&2
echo "BUG: This line should not be reached, program should have exited." >&2
echo "Returned data from Sub Test is $returnFromSubTest." >&2
echo "----------------------------------------------------------------" >&2
fi
echo "Returning data from function." >&2
returnData="Some_Data"
echo $returnData
}
# ----------------------------------------------
# Function: SUBTEST for a problem and kill the
# program if there is a problem. This will be
# called as a sub-sub-routine from the
# TestForBadProblem function.
# ----------------------------------------------
SubTestForProblem()
{
echo "SubTesting Now (subroutine from subroutine)..." >&2
if "$subProblem" = true
then
# If the work-around is working as I expect it to work,
# the program should be killed here just as effectively
# as if I had killed it from the parent function. But
# it's not quite doing that. It seems to complete the
# parent function before killing the program.
echo "There was a subtest problem. Exiting program." >&2
kill -s TERM $SUB_PID
else
echo "No sub problem found. Returning data" >&2
fi
subReturnData="Some_Sub_Data"
echo $subReturnData
}
# ----------------------------------------------
# Main program code body
# ----------------------------------------------
echo "Starting program now." >&2
# ----------------------------------------------
# First test - There should be no problem and it
# should return data from the function.
# ----------------------------------------------
echo "" >&2
echo "First test - No problem encountered, data retrieved." >&2
badProblem=false
subProblem=false
returnedData=$( TestForBadProblem )
echo "returnedData was: $returnedData" >&2
# ----------------------------------------------
# Test 1.5 - Try a sub-function
# ----------------------------------------------
echo "" >&2
echo "1.5 test - Only subProblem encountered, data retrieved." >&2
badProblem=false
subProblem=true
returnedData=$( TestForBadProblem )
echo "returnedData was: $returnedData" >&2
# ----------------------------------------------
# Second test - There should be a bad problem
# encountered and it should quit the program
# without trying to return any data at all.
# It shouldn't even reach the line that tries
# to echo the returned data.
# ----------------------------------------------
echo "" >&2
echo "Second test - Problem encountered, attempt to retrieve data, method 1." >&2
badProblem=true
subProblem=false
returnedData=$( TestForBadProblem )
echo "returnedData was: $returnedData" >&2
# ----------------------------------------------
# You should not reach this line of code because
# the program should have exited in the second
# test. However the program continues to this
# point, despite the documentation for "exit"
# indicating that it should truly exit rather
# than just act like a "return" statement.
# ----------------------------------------------
echo "" >&2
echo "----------------------------------------------------------------" >&2
echo "BUG: If you can read this, the program did not exit as expected." >&2
echo "----------------------------------------------------------------" >&2
# ----------------------------------------------
# Third test. Try a different way of reading the
# function's return data. This does not work, it
# just sets a string value rather than calling
# the function. The function never gets called.
# ----------------------------------------------
echo "" >&2
echo "Third test - Attempt to retrieve data, method 2." >&2
badProblem=true
subProblem=false
returnedData=TestForBadProblem
echo "returnedData was: $returnedData" >&2
# ----------------------------------------------
# Last test - Call the function but without
# trying to read any return data from it. This
# works as expected, but I want to read the data
# so I can't use this method.
# ----------------------------------------------
echo "" >&2
echo "Last test - Problem encountered, do not attempt to retrieve data." >&2
badProblem=true
subProblem=false
TestForBadProblem
echo "Program will not reach this line, you will not see it." >&2