How do I get effect and usefuless of “set -e” insi

2019-01-08 14:26发布

问题:

set -e (or a script starting with #!/bin/sh -e) is extremely useful to automatically bomb out if there is a problem. It saves me having to error check every single command that might fail.

How do I get the equivalent of this inside a function?

For example, I have the following script that exits immediately on error with an error exit status:

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

The output is as expected:

the following command could fail:

Now I'd like to wrap this into a function:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Expected output:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Actual output:

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

(ie. the function is ignoring set -e)

This presumably is expected behaviour. My question is: how do I get effect and usefuless of set -e inside a shell function? I'd like to be able to set something up such that I don't have to individually error check every call, but the script will stop on encountering an error. It should unwind the stack as far as is needed until I do check the result, or exit the script itself if I haven't checked it. This is what set -e does already, except it doesn't nest.

I've found the same question asked outside Stack Overflow but no suitable answer.

回答1:

From documentation of set -e:

When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value > 0, and is not part of the compound list following a while, until, or if keyword, and is not a part of an AND or OR list, and is not a pipeline preceded by the ! reserved word, then the shell shall immediately exit.

In your case, false is a part of a pipeline preceded by ! and a part of if. So the solution is to rewrite your code so that it isn't.

In other words, there's nothing special about functions here. Try:

set -e
! { false; echo hi; }


回答2:

You may directly use a subshell as your function definition and set it to exit immediately with set -e. This would limit the scope of set -e to the function subshell only and would later avoid switching between set +e and set -e.

In addition, you can use a variable assignment in the if test and then echo the result in an additional else statement.

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a


回答3:

This is a bit of a kludge, but you can do:

export -f f
if sh -ec f; then 
...

This will work if your shell supports export -f (bash does).

Note that this will not terminate the script. The echo after the false in f will not execute, nor will the body of the if, but statements after the if will be executed.

If you are using a shell that does not support export -f, you can get the semantics you want by running sh in the function:

f() { sh -ec '
  echo This will execute
  false
  echo This will not
  '
}


回答4:

I eventually went with this, which apparently works. I tried the export method at first, but then found that I needed to export every global (constant) variable the script uses.

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.

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

You can't run f as part of a pipeline, or as part of a && of || command list (except as the last command in the pipe or list), or as the condition in an if or while, or other contexts that ignore set -e. This code also can't be in any of those contexts, so if you use this in a function, callers have to use the same subshell / save-exit-status trickery. This use of set -e for semantics similar to throwing/catching exceptions is not really suitable for general use, given the limitations and hard-to-read syntax.

trap err_handler_function ERR has the same limitations as set -e, in that it won't fire for errors in contexts where set -e won't exit on failed commands.

You might think the following would work, but it doesn't:

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -e doesn't take effect inside the subshell because it remembers that it's inside the condition of an if. I thought being a subshell would change that, but only being in a separate file and running a whole separate shell on it would work.



回答5:

This is by design and POSIX specification. We can read in man bash:

If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

therefore you should avoid relying on set -e within functions.

Given the following exampleAustin Group:

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

the set -e is ignored within the function, because the function is a command in an AND-OR list other than the last.

The above behaviour is specified and required by POSIX (see: Desired Action):

The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.



回答6:

Join all commands in your function with the && operator. It's not too much trouble and will give the result you want.



回答7:

I know this isn't what you asked, but you may or may not be aware that the behavior you seek is built into "make". Any part of a "make" process that fails aborts the run. It's a wholly different way of "programming", though, than shell scripting.



回答8:

You will need to call your function in a sub shell (inside brackets ()) to achieve this.

I think you want to write your script like this:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Then the output is (as desired):

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function


标签: shell sh