Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash: error handling and functions

Tags:

bash

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.

like image 576
kilianc Avatar asked Nov 21 '22 23:11

kilianc


1 Answers

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
like image 125
kilianc Avatar answered Nov 24 '22 00:11

kilianc