Unoffical empeg BBS

Quick Links: Empeg FAQ | RioCar.Org | Hijack | BigDisk Builder | jEmplode | emphatic
Repairs: Repairs

Topic Options
#371509 - 13/01/2019 10:18 Bash script help - Exiting from within a function that returns data?
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Are there any experts in Bash script here that can help?

I'm writing a script to run in the Task Scheduler in my Synology NAS, but my programming pattern for one particular part of it is giving me fits. The "exit" command in Bash behaves in a way that I would call "weird" compared to what I would expect.

In one situation, "exit" behaves more like a "return" statement. Clearly it's because Bash is similar to Powershell, which I have much more experience with, and I'm expecting Bash to behave like Powershell here, but it doesn't. So clearly I'm doing this wrong because I have incorrect expectation, but in this case, what is the right way to do this programming pattern instead?

I've done some googling, and everyone says "exit should exit the program, period". But that's not what's happening when I try it in this one particular situation.

Below is the example script. I'm hoping that it's self-explanatory. Thanks in advance for any help or suggestions.

Code:

#!/bin/bash

# ----------------------------------------------
# Program to demonstrate an issue with Bash.
# ----------------------------------------------

# ----------------------------------------------
# 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()
{
  echo "Testing for a potential bad problem..."  >&2
  if "$badProblem" = true
  then
    echo "There was a bad problem. Exiting program now." >&2
    exit 1
  else
    echo "No bad problem found. Returning data from function." >&2
  fi

  returnData="Some_Data"
  echo $returnData
}

# ----------------------------------------------
# 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 "First test - No problem encountered, data retrieved." >&2
badProblem=false
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 "Second test - Problem encountered, attempt to retrieve data, method 1." >&2
badProblem=true
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 "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 "Third test - Attempt to retrieve data, method 2." >&2
badProblem=true
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 "Last test - Problem encountered, do not attempt to retrieve data." >&2
badProblem=true
TestForBadProblem
echo "Program will not reach this line, you will not see it." >&2





The output from the above program looks like this:



Code:
Starting program now.
First test - No problem encountered, data retrieved.
Testing for a potential bad problem...
No bad problem found. Returning data from function.
returnedData was: Some_Data
Second test - Problem encountered, attempt to retrieve data, method 1.
Testing for a potential bad problem...
There was a bad problem. Exiting program now.
returnedData was: 
----------------------------------------------------------------
BUG: If you can read this, the program did not exit as expected.
----------------------------------------------------------------
Third test - Attempt to retrieve data, method 2.
returnedData was: TestForBadProblem
Last test - Problem encountered, do not attempt to retrieve data.
Testing for a potential bad problem...
There was a bad problem. Exiting program now.
_________________________
Tony Fabris

Top
#371510 - 13/01/2019 10:54 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
It occurs to me that I could solve the problem by not trying to return data from the function at all. Instead, the function could just write the data to a global variable and then the caller could look at the contents of that global variable (since all variables in Bash are global by default unless I take steps to make them otherwise).

This would work around the issue, but it makes me cringe. I feel like there's got to be a way to do it in the more correct/structured fashion, if only I knew what it was. Still, if that's the only way to do it in Bash, so be it.
_________________________
Tony Fabris

Top
#371511 - 13/01/2019 12:58 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
andy
carpal tunnel

Registered: 10/06/1999
Posts: 5914
Loc: Wivenhoe, Essex, UK
This SO question seems to cover it https://stackoverflow.com/questions/9893...xecution-no-mat

(and one of the suggested answer is a global)
_________________________
Remind me to change my signature to something more interesting someday

Top
#371512 - 13/01/2019 13:07 Re: Bash script help - Exiting from within a function that returns data? [Re: andy]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Thank you! Indeed that person is encountering the same problem I am. My searches didn't locate that SO article when I was looking. Thanks for finding it.

The green checkmark answer looks like it will solve my problem. I don't understand it, though, it's doing stuff syntactically that I'm not familiar with. More bash stuff for me to learn. smile
_________________________
Tony Fabris

Top
#371513 - 13/01/2019 13:17 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
peter
carpal tunnel

Registered: 13/07/2000
Posts: 4172
Loc: Cambridge, England
Invoking exit always terminates the shell. But $(...) forks and runs the function in a sub-shell, so in that case it's the sub-shell which terminates. You can use $? to read the status (success or failure) of the most recent sub-shell, or add "set -e" to your script to make it error on failing sub-shells.

Peter

Top
#371514 - 13/01/2019 13:51 Re: Bash script help - Exiting from within a function that returns data? [Re: peter]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Thanks! That makes sense. Like the person in the StackOverflow article, I couldn't use those methods in my actual program, but the "green checkmark" solution's trick of setting up a trap at the top level and invoking the trap from within the function worked for what I needed to do. Thank you so much!
_________________________
Tony Fabris

Top
#371529 - 15/01/2019 18:37 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Now I want to apply the same pattern to a subroutine of a subroutine, and the original workaround is not working for that case. I'm poking at it, but if anyone has any ideas on that one too, I'd love to hear. smile
_________________________
Tony Fabris

Top
#371530 - 15/01/2019 19:34 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
mlord
carpal tunnel

Registered: 29/08/2000
Posts: 14472
Loc: Canada
It should be working, or are you just trying to kill the subroutines without killing the main script?

Top
#371531 - 15/01/2019 20:02 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
I've figured out that I'm able to complete my current task without needing to have the sub-sub-routine kill the program. My particular sub-sub-routine can, in my application, return a success/fail exit code and I can use its parent function to terminate the program using the original workaround if needed.

So I've got my situation sorted, but for future reference and knowledge, I'm wondering how to leverage the original workaround to do the same thing in the sub-sub-routine. Perhaps if I understood the syntax, I could figure it out. Here's what I see so far.

The workaround code (from here) is:
Code:
# Top of the main script body
trap "exit 1" TERM
export TOP_PID=$$

# Function definition
TestForBadProblem()
{
  if "$badProblem" = true
  then
    kill -s TERM $TOP_PID
  fi
  returnData="Some_Data"
  echo $returnData
}

# Main program code body
returnedData=$( TestForBadProblem )


All right, taking it line by line...

Code:
trap "exit 1" TERM

It appears as though it's setting up an interrupt, where, if it received the signal "TERM" it will execute the code "exit 1" in the context of the script that defined the trap.

Code:
export TOP_PID=$$

Create (or perhaps update?) an environment variable named TOP_PID and set it to the process ID of the current script.

Code:
kill -s TERM $TOP_PID

This looks like it's trying to kill the top-level script process, specifying the signal spec "TERM" so that the trap handler catches it, and specifying the process ID of the top-level script process, by using the environment variable which contains the top level script's PID.

Based on what I see, the method should have worked. If I add a sub-sub routine that contains the line "kill -s TERM $TOP_PID" it should kill the top level script. That's not what's happening though. What seems to happen is that the parent function completes what it's doing and only then the script terminates.

Here is the code and output from my updated example script.

Code:
Code:
#!/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()
{
  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 $TOP_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



Output from the code above:

Code:
Starting program now.

First test - No problem encountered, data retrieved.
Testing for a potential bad problem...
No bad problem found.
SubTesting Now (subroutine from subroutine)...
No sub problem found. Returning data
Returning data from function.
returnedData was: Some_Data

1.5 test - Only subProblem encountered, data retrieved.
Testing for a potential bad problem...
No bad problem found.
SubTesting Now (subroutine from subroutine)...
There was a subtest problem. Exiting program.
----------------------------------------------------------------
BUG: This line should not be reached, program should have exited.
Returned data from Sub Test is Some_Sub_Data.
----------------------------------------------------------------
Returning data from function.
_________________________
Tony Fabris

Top
#371532 - 15/01/2019 20:05 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Re-reading my post, I have a hunch as to what might be happening.

When it hits the trap, it must be executing the trap's "exit 1" in the context of the parent function "TestForBadProblem" instead of in the context of the main top level script.

I may have to define a second trap that invokes the first trap.

EDIT: No, that didn't work. Here's my updated code and output:

Code:
#!/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


Code:
Starting program now.

First test - No problem encountered, data retrieved.
Testing for a potential bad problem...
No bad problem found.
SubTesting Now (subroutine from subroutine)...
No sub problem found. Returning data
Returning data from function.
returnedData was: Some_Data

1.5 test - Only subProblem encountered, data retrieved.
Testing for a potential bad problem...
No bad problem found.
SubTesting Now (subroutine from subroutine)...
There was a subtest problem. Exiting program.
----------------------------------------------------------------
BUG: This line should not be reached, program should have exited.
Returned data from Sub Test is Some_Sub_Data.
----------------------------------------------------------------
Returning data from function.
_________________________
Tony Fabris

Top
#371533 - 15/01/2019 20:23 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
andy
carpal tunnel

Registered: 10/06/1999
Posts: 5914
Loc: Wivenhoe, Essex, UK
Originally Posted By: tfabris
Now I want to apply the same pattern to a subroutine of a subroutine, and the original workaround is not working for that case. I'm poking at it, but if anyone has any ideas on that one too, I'd love to hear. smile


Learn python...
_________________________
Remind me to change my signature to something more interesting someday

Top
#371534 - 15/01/2019 20:25 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Hm, I added ECHOs to print out the PIDs and they were the same PID in both cases, whether defined at the top level or in the function.

Some googling says that $! should get me the PID of the function, but it's not working and just returning nothing in my instance.
_________________________
Tony Fabris

Top
#371535 - 15/01/2019 20:28 Re: Bash script help - Exiting from within a function that returns data? [Re: andy]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Originally Posted By: andy
Learn python...


A valid thing to try.

The place I need this script to run is inside a "user-defined script" inside the Task Scheduler of a Synology NAS. Those default to Bash scripts, but maybe I can get Python to work there too.

For now though I'll do my existing work-around which is to do all the killing in the one function.
_________________________
Tony Fabris

Top
#371544 - 16/01/2019 10:44 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
Roger
carpal tunnel

Registered: 18/01/2000
Posts: 5680
Loc: London, UK
Control Panel -> Task Scheduler -> TASK -> Edit Task -> Task Settings -> Run command:

Code:
python /volume1/homes/roger/hello.py


Code:
admin@ds416:/volume1/homes/roger$ cat hello.py 
import sys
print(sys.version)


Resulting email notification:

Code:
Dear user,

Task Scheduler has completed a scheduled task.

Task: Example for tfabris
Start time: Wed, 16 Jan 2019 10:42:51 GMT
Stop time: Wed, 16 Jan 2019 10:42:52 GMT
Current status: 0 (Normal)
Standard output/error:
2.7.12 (default, Sep  7 2018, 13:17:37) 
[GCC 4.9.3 20150311 (prerelease)]


Sincerely,
Synology DiskStation
_________________________
-- roger

Top
#371545 - 16/01/2019 10:50 Re: Bash script help - Exiting from within a function that returns data? [Re: andy]
Roger
carpal tunnel

Registered: 18/01/2000
Posts: 5680
Loc: London, UK
Originally Posted By: andy
Learn python...


Or Ruby, or Perl...

Or even Java, node.js (but only up to 8.x, not the latest 10/11.x) or PHP, which are all supported by Synology.
_________________________
-- roger

Top
#371548 - 16/01/2019 13:18 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
mlord
carpal tunnel

Registered: 29/08/2000
Posts: 14472
Loc: Canada
For the shell script, I use /bin/bash for my scripts here.
With bash, the $BASHPID variable can be used to obtain the PID of a subshell (function), so you could use that. Eg. SUB_PID=$BASHPID

Next note: After invoking "kill" (which doesn't "kill" anything, it just sends a signal), your functions continue to execute and return data. If you don't want that to happen, then add an "exit 1" line after each "kill" line.

When I remove the "trap" line from the TestForBadProblem() function, your script appears to work just fine for me here. How about there?

Top
#371551 - 16/01/2019 18:42 Re: Bash script help - Exiting from within a function that returns data? [Re: mlord]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Thanks so much, Roger and Mark!

Roger - That's excellent instructions regarding how to run Python in the Synology task scheduler. Thank you! It also reminds me that I can put my scripts directly onto the NAS's hard disk rather than pasting them in as code into the task scheduler. I was having trouble keeping it under the 8192-byte limit of that field, and this would simplify the process of iterating on the script versions.

Mark - Thanks so much, I'll try those things. Your note that "Kill" only sends a signal and that adding "exit 1" after the "Kill" command will stop the function from continuing to execute, is super useful. I'll give those things a try.

I don't know what you mean by removing the "trap" line makes the script seem to work... I don't see how yet, but I'll try it.

Thanks so much!
_________________________
Tony Fabris

Top
#371553 - 16/01/2019 20:15 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
mlord
carpal tunnel

Registered: 29/08/2000
Posts: 14472
Loc: Canada
The script had two of those trap lines, one global near the top, and a second one inside a sub function. Removing the latter made the script exit as/when required for me, but I don't fully understand what you expect from it. smile

Top
#371554 - 16/01/2019 21:11 Re: Bash script help - Exiting from within a function that returns data? [Re: mlord]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
Yeah, the second trap wasn't working correctly as I expected it to work, regardless of whether I had it in there or I removed it. After poking at it a bit more, and having a better understanding of things, I think I have a final solution. Hopefully this new version of the demonstration script will make it more clear.

In particular, your notes were very helpful in making me understand this more clearly:

- $BASHPID was very important. I'd seen it in my google search results, but hadn't seen anyone clearly explain that it would get me the PID of the current function being called, in my situation. Everyone described it differently. In particular, everyone in my google search results seemed to indicate that I needed to use "$!" to get that value that I was looking for, and that $BASHPID would get me something completely different. Now that I know the right value to use, it helped immensely.

- Understanding that the KILL signal didn't kill anything, and that I still needed an "exit 1" after the kill, was super-critical to getting the code to work right for me. I now see that the KILL signal does not interrupt the program flow of the child. It only executes the trap code after the child process has exited. I expected it to terminate things right away, and it doesn't work like that. For this reason, I still need that trap defined inside the sub function. My only problem before was that the trap was mis-coded, it needed that $BASHPID and some other tricks before it could work right.


Here's my updated example script which hopefully shows clearly how it all works now. Thanks again, everyone, for your excellent help and clear explanations.

Code:
#!/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
_________________________
Tony Fabris

Top
#371858 - 29/03/2019 22:56 Re: Bash script help - Exiting from within a function that returns data? [Re: tfabris]
tfabris
carpal tunnel

Registered: 20/12/1999
Posts: 31563
Loc: Seattle, WA
_________________________
Tony Fabris

Top