tijagi
tijagi

Reputation: 1244

How to make RETURN trap in bash preserve the return code?

Below is the simplified scheme of the script I am writing. The program must take parameters in different ways, so there is a fine division to several functions.

The problem is that the chainloading of the return value from deeper functions breaks on the trap, where the result is to be checked to show a message.

#! /usr/bin/env bash

check_a_param() {
    [ "$1" = return_ok ] && return 0 || return 3
}

check_params() {
    # This trap should catch negative results from the functions
    #   performing actual checks, like check_a_param() below.
    return_trap() {
        local retval=$?
        [ $retval -ne 0 ] && echo 'Bad, bad… Dropping to manual setup.'
        return $retval
    }
    # check_params can be called from different functions, not only
    #   setup(). But the other functions don’t care about the return value
    #   of check_params().
    [ "${FUNCNAME[1]}" = setup ] \
        && trap "return_trap; got_retval=$?; trap - RETURN; return $got_retval;" RETURN
    check_a_param 'return_bad' || return $?
    # …
    # Here we check another parameters in the same way.
    # …
    echo 'Provided parameters are valid.'
    return 0  # To be sure.
}

ask_for_params() {
    echo 'User sets params manually step by step.'
}

setup() {
    [ "$1" = force_manual ] && local MANUAL=t
    # If gathered parameters do not pass check_params()
    #   the script shall resort to asking user for entering them.
    [ ! -v MANUAL ] && {
        check_params \
            && echo "check_params() returned with 0. Not running manual setup."
            || false
    }|| ask_for_params
    # do_the_job
}

setup "$@"  # Either empty or ‘force_manual’.

How it should work:

              ↗ 3 → 3→ trap →3                     ↗ || ask_for_params ↘
 check_a_param >>> check_params >>> [ ! -v MANUAL ]                     ↓
              ↘ 0 → 0→ trap →0                     ↘ && ____________ do_the_job

The idea is, if a check fails, its return code forces check_params() to return, too, which, in its turn would trigger the || ask_for_params condition in setup(). But the trap returns 0:

              ↗ 3 → 3→ trap →0
 check_a_param >>> check_params >>> [ ! -v MANUAL ] &&… >>> do_the_job
              ↘ 0 → 0→ trap →0

If you try to run the script as is, you should see

Bad, bad… Dropping to manual setup.
check_params() returned with 0. Not running manual setup.

Which means that the bad result triggered the trap(!) but the mother function that has set it, didn’t pass the result.

In attempt to set a hack I’ve tried

Now we can finally see that the last return directive which reads the number from the file, actually returns 3. But check_params() still returns 0!

++ trap - RETURN
++ return 3
+ retval2=0
+ echo 'check_params() returned with 0. Not running manual setup.'
check_params() returned with 0. Not running manual setup.

If the argument to the trap command is strictly a function name, it returns the original result. The original one, not what return_trap() returns. I’ve tried to increment the result and still got 3. You may also ask ‘Why would you need to unset the trap so much?’. It’s to avoid another bug, which causes the trap to trigger every time, even when check_params() is called from another function. Traps on RETURN are local things, they aren’t inherited by another functions unless there’s debug or trace flags explicitly set on them, but it looks like they keep traps set on them between runs. Or bash keeps traps for them. This trap should only be set when check_params() is called from a specific function, but if the trap is not unset, it continues to get triggered every time check_a_param() returns a value greater than zero independently of what’s in FUNCNAME[1].

Here I give up, because the only exit I see now is to implement a check on the calling function before each || return $? in check_params(). But it’s so ugly it hurts my eyes.

I may only add that, $? in the line setting the trap will always return 0. So, if you, for example, declare a local variable retval in return_trap(), and put such code to check it

trap "return_trap; [ -v retval ]; echo $?; trap - RETURN; return $retval" RETURN

it will print 0 regardless of whether retval is actually set or not, but if you use

trap "return_trap; [ -v retval ] && echo set || echo unset; trap - RETURN; return $retval" RETURN

It will print ‘unset’.


GNU bash, version 4.3.39(1)-release (x86_64-pc-linux-gnu)

Upvotes: 10

Views: 6388

Answers (1)

tijagi
tijagi

Reputation: 1244

Funny enough,

trap "return_trap; trap - RETURN" RETURN

simply works.

[ ! -v MANUAL ] && {
    check_params; retval2=$?
    [ $retval2 -eq 0 ] \
        && echo "check_params() returned with 0. Not running manual setup." \
        || false
}|| ask_for_params

And here’s the trace.

+ check_a_parameter return_bad
+ '[' return_bad = return_ok ']'
+ return 3
+ return 3
++ return_trap
++ local retval=3
++ echo 3
++ '[' 3 -ne 0 ']'
++ echo 'Bad, bad… Dropping to manual setup.'
Bad, bad… Dropping to manual setup.
++ return 3
++ trap - RETURN
+ retval2=3
+ '[' 3 -eq 0 ']'
+ false
+ ask_for_params
+ echo 'User sets params manually step by step.'
User sets params manually step by step.

So the answer is simple: do not try to overwrite the result in the line passed to the trap command. Bash handles everything for you.

Upvotes: 11

Related Questions