Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

  • to set retval as a global variable declare -g retval=$? in the return_trap() and use its value in the line setting the trap. The variable is set ([ -v retval ] returns successfully), but …has no value. Funny.
  • okay, let’s putretval=Eeh to the check_params(), outside the return_trap() and just set it to $? as a usual param. Nope, the retval in the function doesn’t set the value for the global variable, it stays ‘Eeh’. No, there’s no local directive. It should be treated as global by default. If you put test=1 to check_params() and test=3 in check_a_param() and then print it with echo $testat the end of setup(), you should see 3. At least I do. declare -g doesn’t make any difference here, as expected.
  • maybe that’s the scope of the function? No, that’s not it either. Moving return_trap() along with declare -g retval=Eeh doesn’t make any difference.
  • when the modern software means fall, it’s time to resort to good old writing to a file. Let’s print the retval to /tmp/t with retval=$?; echo $retval >/tmp/t in return_trap() and read it back with

    trap "return_trap; trap - RETURN; return $(</tmp/t)" RETURN

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)

like image 720
tijagi Avatar asked Apr 06 '16 05:04

tijagi


People also ask

How do you trap in bash?

To set a trap in Bash, use trap followed by a list of commands you want to be executed, followed by a list of signals to trigger it. For complex actions, you can simplify trap statements with Bash functions.

What does return 0 do in bash?

0 means no errors. Any other value means something went wrong; even negative values can be returned. AFAIK there is no other standard for return values except for 0. If you want to return a value that is not 0, it is up to you to tell the users of your program what different values/error codes mean.

How do I return something in bash?

The return status can be specified by using the return keyword, and it is assigned to the variable $? . The return statement terminates the function. You can think of it as the function's exit status . #!/bin/bash my_function () { echo "some result" return 55 } my_function echo $?

What is trap exit in bash?

The secret sauce is a pseudo-signal provided by bash, called EXIT, that you can trap; commands or functions trapped on it will execute when the script exits for any reason. Let's see how this works. The basic code structure is like this: #!/bin/bash.


1 Answers

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.

like image 129
tijagi Avatar answered Sep 23 '22 21:09

tijagi