8

I am trying to call a function in a loop and gracefully handle and continue when it throws.

If I omit the || handle_error it just stops the entire script as one would expect.

If I leave || handle_error there it will print foo is fine after the error and will not execute handle_error at all. This is also an expected behavior, it's just how it works.

#!/bin/bash

set -e

things=(foo bar)

function do_something {
  echo "param: $1"

  # just throw on first loop run
  # this statement is just a way to selectively throw
  # not part of a real use case scenario where the command(s)
  # may or may not throw
  if [[ $1 == "foo" ]]; then
    throw_error
  fi

  # this line should not be executed when $1 is "foo"
  echo "$1 is fine."
}

function handle_error {
  echo "$1 failed."
}

for thing in ${things[@]}; do
  do_something $thing || handle_error $thing
done

echo "done"

yields

param: foo
./test.sh: line 12: throw_error: command not found
foo is fine.
param: bar
bar is fine.
done

what I would like to have is

param: foo
./test.sh: line 12: throw_error: command not found
foo failed.
param: bar
bar is fine.
done

Edit:

do_something doesn't really have to return anything. It's just an example of a function that throws, I could potentially remove it from the example source code because I will have no control over its content nor I want to, and testing each command in it for failure is not viable.

Edit:

You are not allowed to touch do_something logic. I stated this before, it's just a function containing a set of instructions that may throw an error. It may be a typo, it may be make failing in a CI environment, it may be a network error.

kilianc
  • 7,397
  • 3
  • 26
  • 37
  • do_something $thing || this requires a function return, not an echo (well it can echo all you want, but the echo is not a return), from the function, to register with a fail action. As in: on failure, return 1 or > 1 up to 255 (I think it's 255). foo is a command, but there is no foo. Maybe what you want is: if [[ $1 == "foo" ]]; then return 1;fi || is looking for an integer > 0 to trigger the error condition. – Lizardx Nov 14 '15 at 02:00
  • @Lizardx imagine that `do_something` is a very complicated function with 30+ lines. I have no control on which line is going to fail, return is not viable. – kilianc Nov 14 '15 at 02:02
  • Sure it is, and sure you have full control, you put an integer into a varlable in the function and return the integer, set the starting to 0, if no failure happens, the return returns the 0, like: return $ret_value || means it's looking for the return value. foo is nothing so it doesn't have anything to do with this. Here you would do say, if 1 = foo, then ret_value=1;fi then later, return $ret_value. Otherwise it's irrelevant what you want from what I can see, bash works like this. – Lizardx Nov 14 '15 at 02:04
  • I changed `foo` into `throw_error` to make it more clear that is just a way to throw an error. I want my function to stop on first error and handle it in the loop. Basically a try catch like construct. What you are suggesting doesn't work for my use case. I can't if-then-else each line. – kilianc Nov 14 '15 at 02:12
  • Possible duplicate of [“set -e” in a function](http://stackoverflow.com/questions/5754845/set-e-in-a-function). Unless you can add `return 1` where needed inside the function, the main thing this depends on is `set -e` working at all inside a function (which it doesn't, in bash). – Peter Cordes Nov 14 '15 at 04:34
  • Oops, actually `set -e` works inside functions, but *not* if the function-call is part of a compound-command or condition, like `func && handler_error`, or `if func;then`. The accepted answer to the question I linked works great, though. disable `set -e`, then run the function call inside a subshell that has `set -e` enabled. Save the exit status of the subshell in a variable, re-enable `set -e`, then test the var. e.g. `if ((err_status));then handle_error "$thing";done`. Also, don't forget to double-quote all your variable expansions, including `"${things[@]}"` – Peter Cordes Nov 14 '15 at 04:44
  • @PeterCordes I added your comment to my answer, so far is the nicest way to do it without having the function in a separate file with `set -e` on tis own. Thanks! – kilianc Nov 14 '15 at 23:15

3 Answers3

2

The solution I found is to save the function in a separate file and execute it in a sub-shell. The downside is that we lose all locals.

do-something.sh

#!/bin/bash

set -e

echo "param: $1"

if [[ $1 == "foo" ]]; then
  throw_error
fi

echo "$1 is fine."

my-script.sh

#!/bin/bash

set -e

things=(foo bar)

function handle_error {
  echo "$1 failed."
}

for thing in "${things[@]}"; do
  ./do-something.sh "$thing" || handle_error "$thing"
done

echo "done"

yields

param: foo
./do-something.sh: line 8: throw_error: command not found
foo failed.
param: bar
bar is fine.
done

If there is a more elegant way I will mark that as correct answer. Will check again in 48h.

Edit

Thanks to @PeterCordes comment and this other answer I found another solution that doesn't require to have separate files.

#!/bin/bash

set -e

things=(foo bar)

function do_something {
  echo "param: $1"

  if [[ $1 == "foo" ]]; then
    throw_error
  fi

  echo "$1 is fine."
}

function handle_error {
  echo "$1 failed with code: $2"
}

for thing in "${things[@]}"; do
  set +e; (set -e; do_something "$thing"); error=$?; set -e
  ((error)) && handle_error "$thing" $error
done

echo "done"

correctly yields

param:  foo
./test.sh: line 11: throw_error: command not found
foo failed with code: 127
param:  bar
bar is fine.
done
Community
  • 1
  • 1
kilianc
  • 7,397
  • 3
  • 26
  • 37
  • You're still not quoting `"${things[@]}"` or `"$thing"` properly. And if you're going to use bash-only features like `[[ ]]`, you might as well use `((error)) && handle_error "$thing" "$error"`. I think arithmetic contexts are a nice way to work with boolean variables, like `((boolean)) || echo false`. See http://stackoverflow.com/a/26920580/224132 – Peter Cordes Nov 14 '15 at 23:21
0
#!/bin/bash

set -e

things=(foo bar)

function do_something() {
  echo "param: $1"
  ret_value=0

  if [[ $1 == "foo" ]]; then
    ret_value=1
  elif [[ $1 == "fred" ]]; then
    ret_value=2
  fi

  echo "$1 is fine."
  return $ret_value
}

function handle_error() {
  echo "$1 failed."
}

for thing in ${things[@]}; do
  do_something $thing || handle_error $thing
done

echo "done"

See my comment above for the explanation. You can't test for a return value without creating a return value, which should be somewhat obvious. And || tests for a return value, basically, one greater than 0. Like && tests for 0. I think that's more or less right. I believe the bash return value limit is 254? I want to say. Must be integer between 0 and 254. Can't be a string, a float, etc.

http://tldp.org/LDP/abs/html/complexfunct.html

Functions return a value, called an exit status. This is analogous to the exit status returned by a command. The exit status may be explicitly specified by a return statement, otherwise it is the exit status of the last command in the function (0 if successful, and a non-zero error code if not). This exit status may be used in the script by referencing it as $?. This mechanism effectively permits script functions to have a "return value" similar to C functions.

So actually, foo there would have returned a 127 command not found error. I think. I'd have to test to see for sure.

[updated} No, echo is the last command, as you can see from your output. And the outcome of the last echo is 0, so the function returns 0. So you want to dump this notion and go to something like trap, that's assuming you can't touch the internals of the function, which is odd.

echo fred; echo reval: $?
fred
reval: 0

What does set -e mean in a bash script?

-e  Exit immediately if a command exits with a non-zero status.

But it's not very reliable and considered as a bad practice, better use :

trap 'do_something' ERR

http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html see trap, that may do what you want. But not ||, unless you add returns.

Community
  • 1
  • 1
Lizardx
  • 1,165
  • 10
  • 16
  • Comments are not permanent, so better for you to copy the comment into your answer. – Mogsdad Nov 14 '15 at 02:13
  • I appreciate you trying to help, but you are missing the point of my question. Somewhere `do_something` is going to fail and I want to handle it in the loop. I am not particularly attached to `|| handle_error` I just want a way to handle `do_something` throwing. I don't care about return value, unless there is a way to return 1 if an error happens. Storing each line return value is not viable. – kilianc Nov 14 '15 at 02:16
  • You should rewrite your question in that case to be close to what your problem actually is, as it stands it makes little sense. You list wanting to get the error return with || yet you say you don't care. I suggest thinking a bit more and redoing your question so it can be understood. If you capture the error of anything in that do_something and return it, that does what you want, as I said. To handle the error you should be catching the error, and doing something with it, inside do_something. Anyway, good luck, I suggest rewriting your code here. – Lizardx Nov 14 '15 at 02:20
  • Naturally there is a way to return 1, by the way, 30 lines of code is nothing, so you just trap an error with each thing inside it, with ||, store it in ret_val, and at the end of the function test if ret_val > 0, and if it is, return 1, else return 0. This isn't complicated stuff. ie: something || ret_value=$? – Lizardx Nov 14 '15 at 02:22
  • Thanks @Lizardx it's not about being complicated this is about doing it in an elegant and smart way. I am editing my question to clarify and avoid further confusion. – kilianc Nov 14 '15 at 02:23
-1

try

if [[ "$1" == "foo" ]]; then
  foo
fi

I wonder if it was trying to execute the command foo within the condition test?

from bash reference:

-e Exit immediately if a pipeline (see Pipelines), which may consist of a single simple command (see Simple Commands), a list (see Lists), or a compound command (see Compound Commands) returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.

as you can see, if the error occurs within the test condition, then the script will continue oblivious and return 0.

--

Edit

So in response, I note that the docs continue:

If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit.

Well because your for is succeeded by the echo, there's no reason for an error to be thrown!

Sanjay Manohar
  • 6,920
  • 3
  • 35
  • 58
  • I updated my question since your answer was focusing on the wrong thing. It's now clear why it misbehaves at least, thanks for the docs. – kilianc Nov 14 '15 at 02:00
  • 2
    "The shell does not exit if the command that fails is ... part of any command executed in a ... || list". `throw_error` is called from `do_something`, which precedes `||`, so the shell does not exit. After the shell prints the "command not found" error, the function proceeds as normal with `echo "$1 is fine"`. – chepner Nov 14 '15 at 02:25
  • @chepner yes and that's because I am not using `-o pipefail` right? – kilianc Nov 14 '15 at 02:29
  • No, `pipefail` isn't relevant here, because you don't have any (nontrivial) pipelines.`pipefail` makes `false | true` exit 1 instead of 0. Whether or not that option is set, a command that fails on, or from, the left-hand side of `||` does not exit the shell. – chepner Nov 14 '15 at 02:33
  • @chepner ok, makes sense – kilianc Nov 14 '15 at 02:43