linuxbashshellterminalzsh

How to trap RETURN in a sourced BASH script from Zsh


I am writing a script that must be sourced and that I use daily in the terminal (zsh), and the solution to one of a problem in the script is to unlock a flock lock, I would do it in the trap for RETURN.

But, it seems the trap <code> RETURN syntax does not work in Zsh as it does in Bash.

I have this simple script here, test.sh:

#!/bin/bash

trap 'echo returned' RETURN

echo "Hello, World!"

Here what happens when I source it with Zsh:


[urpagin:~/sandbox/tmp_sbox/F0MPlT]$ echo "$0"
/usr/bin/zsh
[urpagin:~/sandbox/tmp_sbox/F0MPlT]$ source test.sh
test.sh:trap:3: undefined signal: RETURN
Hello, World!

Here what happens when I source it with BASH:

[urpagin@ikari F0MPlT]$ echo "$0"
bash
[urpagin@ikari F0MPlT]$ source test.sh
Hello, World!
returned

And with another style that seems to be from Zsh, we define test2.sh:

#!/bin/zsh

TRAPEXIT() {
  echo "Exited"
}

TRAPRETURN() {
  echo "Returned"
}

echo "Hello, World!"

We can see that when run like a normal script using zsh, the TRAPEXIT() function gets called, whereas the TRAPRETURN() function does not.

[urpagin:~/sandbox/tmp_sbox/F0MPlT]$ echo "$0"
/usr/bin/zsh
[urpagin:~/sandbox/tmp_sbox/F0MPlT]$ zsh test2.sh     
Hello, World!
Exited
[urpagin:~/sandbox/tmp_sbox/F0MPlT]$ source test2.sh
Hello, World!
Exited

We also observe that the TRAPRETURN() function is never called, even when we source the file. The expected behavior would be that the function gets called.

So it seems:

So then, is it even possible to execute code when a sourced script returns?


Solution

  • Zsh doesn't have a trap return construct. Instead, you can simulate it using a custom hook. There's a tiny plugin that helps you define your own hooks: https://github.com/zsh-hooks/zsh-hooks. You don't have to use this, but it helps make defining a custom hook much simpler.

    Back to your example, lets say you have a script foo.sh that you want to work in both Bash and Zsh. It would have the following contents:

    # foo.sh
    trap_return() {
      echo "returned..."
    }
    
    if [[ -n "$ZSH_VERSION" ]]; then
      hooks-add-hook post_source trap_return
    else
      trap 'trap_return' RETURN
    fi
    
    echo "Hello, World!"
    

    Next, you need to wire up the Zsh script you're sourcing foo.sh from. Let's call it bar.zsh

    #!/bin/zsh
    # bar.zsh
    
    source /path/to/zsh-hooks.plugin.zsh
    
    # Define a new way to source that wires up and uses the hook.
    # You could even mask source itself by calling this func `source`.
    source_with_hook() {
      # Define a post_source hook
      hooks-define-hook post_source
    
      # Source the file
      builtin source "$@"
    
      # Run the post_source hook, and remove it for the next call
      hooks-run-hook post_source
      unset post_source
    }
    
    # Now, source foo.sh
    source_with_hook /path/to/foo.sh
    

    Now, let's show that it works in both Bash and Zsh:

    $ bash
    $ echo $BASH_VERSION
    5.3.3(1)-release
    $ source foo.sh
    Hello, World!
    returned...
    $ zsh
    % echo $ZSH_VERSION
    5.9
    % # remember, bar.zsh has our wrapper, and sources foo.sh with our hook
    % source bar.zsh
    Hello, World!
    returned...