4

I try to make a function which can interrupt the script execution (due to fatal error):

quit() {
  echo -e "[ERROR]" | log
  exit 1
}

Call example:

if [ "$#" -eq 1 ]; then
    # Do stuff
else
    echo -e "function getValue: wrong parameters" | log
    quit
fi

Function quit is called (echo in the logfile) but the script keeps going. I've read that exit only terminate the subshell (is that true?) which means that it terminates the quit function but not the entire script.

For now, I prefer not use return code in quit method as it implies a lot of code refactoring.

Is there a way to stop the script from the quit function?

EDIT: full example of a case where the error appears:

#!/bin/bash

logfile="./testQuit_log"

quit() {
  echo "quit" | log
  exit 1
}

log() {
  read data
  echo -e "$data" | tee -a "$logfile"
}


foo() {
  if [ "$#" -ne 1 ]; then
    echo "foo error" | log
    quit
  fi
  echo "res"
}

rm $logfile

var=`foo p1 p2`
var2=`foo p1`

echo "never echo that!" | log

EDIT2: it works correctly when I switch these lines:

var=`foo p1 p2`
var2=`foo p1`

with

var= foo p1 p2
var2= foo p1

Any explanation? Is that because of the subshell?

DavidL
  • 1,120
  • 1
  • 15
  • 34
  • 3
    `exit` will exit the script. The function is not a script. That should work as written. Can you write a short but complete sample that fails to work in the way you are seeing? – Etan Reisner Oct 13 '15 at 15:23
  • 1
    Why are you putting `-e` on all the echos? It's non-portable and does nothing in both the cases you show. (`printf` will interpret C escape sequences in its format string, and is portable. Just remember that unlike echo it does not automatically add a `\n`, so you have to add that to the format if you want it.) – rici Oct 13 '15 at 15:38
  • Question edited. I think I have found the solution, but I'm not quite sure I understand what's happening there... – DavidL Oct 13 '15 at 16:09
  • The problem is that you use command substitution (with the backticks `\``), and that creates a subshell, so the parent doesn't exit. I don't know a good workaround, if you need to keep the output in a variable. Regarding why edit2 works, you forgot the backticks. Your variable should remain unset. – user000001 Oct 13 '15 at 16:18
  • Untested, but I guess you could `kill "$PPID"` before the exit. This is a little hacky though. You'd have to make sure you only call `foo()` in a subshell. – Michael Jaros Oct 13 '15 at 16:21
  • `var=\`foo p1 p2\`` (wrapped in backticks) should throw errors "command foo not found" (or similar), backticks are not quotes. Use `"` double quotes to allow expansion of the contents or `'` single quotes to prevent it. Also, what is `log`? Is there a possibility after you pipe to `log` it doesn't return properly? There is nothing wrong with calling `exit` within a function, so there is something subtle if you are having issues. – David C. Rankin Oct 13 '15 at 16:28
  • @DavidC.Rankin: command `foo` will be found because he declared a function named `foo` above. – user000001 Oct 13 '15 at 16:33
  • Forgive me, I overlooked that completely. Backticks are fine there. – David C. Rankin Oct 13 '15 at 16:38

2 Answers2

5

As it has been outlined in the question's comment section, using exit in a subshell will only exit the subshell and it is not easy to work around this limitation. Luckily, exiting from a subshell or even a function in the same shell is not the best idea anyway:

A good pattern to solve the problem of handling an error on a lower level (like a function or subshell) in a language without exceptions is to return the error instead of terminating the program directly from the lower level:

foo() {
   if [ "$#" -ne 1 ]; then
       echo "foo error" | log
       return 1
   else
       echo "res"
       # return 0 is the default
   fi
} 

This allows control flow to return to the highest level even on error, which is generally considered a good thing (and will incredibly ease debugging complex programs). You can use your function like this:

var=$( foo p1 p2 ) || exit 1
var2=$( foo p1 ) || exit 1

Just to be clear, the || branch is not entered if the assignment fails (it won't), but if the command line inside the command substitution ($( )) returns a non-zero exit code.

Note that $( ) should be used for command substitution instead of backticks, see this related question.

Community
  • 1
  • 1
Michael Jaros
  • 4,586
  • 1
  • 22
  • 39
  • Got it. I was hoping I can avoid to test anything outside of the `quit` function (and handle all the "quit" stuff at one unique place). Looks like I can't... Thanks for your explanation! – DavidL Oct 13 '15 at 20:37
2

Looking at a debug of the script shows the problem. var=`foo p1 p2` forces execution of foo in a subshell (note: the increase in level from + to ++ at the time of the call below) Execution of the script proceeds in a subshell until exit 1 is reached. exit 1 effectively exits the subshell returning to the primary script.

$ bash -x exitstuck.sh
+ logfile=./testQuit_log
+ rm ./testQuit_log
++ foo p1 p2                # var=`foo p1 p2` enters subshell '+ -> ++'
++ '[' 2 -ne 1 ']'
++ echo 'foo error'
++ log                      # log() called
++ read data
++ echo -e 'foo error'
++ tee -a ./testQuit_log
++ quit                     # quit() called
++ echo quit
++ log
++ read data
++ echo -e quit
++ tee -a ./testQuit_log
++ exit 1                   # exit 1 exits subshell, note: '++ -> +'
+ var='foo error
quit'
++ foo p1
++ '[' 1 -ne 1 ']'
++ echo res
+ var2=res
+ log
+ read data
+ echo 'never echo that!'
+ echo -e 'never echo that!'
+ tee -a ./testQuit_log
never echo that!

You can use this to your advantage to accomplish what it is you are trying to do. How? When exit 1 exits the subshell, it does so returning the exit code 1. You can test the exit code in your main script and exit as you intend:

var=`foo p1 p2`
if [ $? -eq 1 ]; then
    exit
fi
var2=`foo p1`

Running in debug again shows the intended operation:

$ bash -x exitstuck.sh
+ logfile=./testQuit_log
+ rm ./testQuit_log
++ foo p1 p2
++ '[' 2 -ne 1 ']'
++ echo 'foo error'
++ log
++ read data
++ echo -e 'foo error'
++ tee -a ./testQuit_log
++ quit
++ echo quit
++ log
++ read data
++ echo -e quit
++ tee -a ./testQuit_log
++ exit 1
+ var='foo error
quit'
+ '[' 1 -eq 1 ']'
+ exit
David C. Rankin
  • 81,885
  • 6
  • 58
  • 85