If I use trap like described e.g. on http://linuxcommand.org/wss0160.php#trap to catch ctrl-c (or similar) and cleanup before exiting then I am changing the exit code returned.

Now this probably won't make difference in the real world (e.g. because the exit codes are not portable and on top of that not always unambiguous as discussed in Default exit code when process is terminated?) but still I am wondering whether there there is really no way to prevent that and return the default error code for interrupted scripts instead?

Example (in bash, but my question shouldn't be considered bash-specific):

trap 'echo EXIT;' EXIT
read -p 'If you ctrl-c me now my return code will be the default for SIGTERM. ' _
trap 'echo SIGINT; exit 1;' INT
read -p 'If you ctrl-c me now my return code will be 1. ' _


$ ./test.sh # doing ctrl-c for 1st read
If you ctrl-c me now my return code will be the default for SIGTERM.
$ echo $?
$ ./test.sh # doing ctrl-c for 2nd read
If you ctrl-c me now my return code will be the default for SIGTERM.
If you ctrl-c me now my return code will be 1. SIGINT 
$ echo $?

(Edited to remove to make it more POSIX-conform.)

(Edited again to make it a bash script instead, my question is not shell-specific though.)

Edited to use the portable "INT" for trap in favor of the non-portable "SIGINT".

Edited to remove useless curly braces and add potential solution.


I solved it now by simply exiting with some error codes hardcoded and trapping EXIT. This might be problematic on certain systems because the error code might differ or the EXIT trap not possible but in my case it's OK enough.

trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
  • Your script looks a bit strange: you tell read to read from the current coprocess and trap cmd SIGINT will not work as the standard says that you should use trap cmd INT.
    – schily
    Commented Oct 12, 2015 at 12:36
  • Ah yes, under POSIX it's of course without the SIG-prefix.
    – phk
    Commented Oct 12, 2015 at 12:40
  • Oops, but then "read -p" wouldn't be supported either, so I am going to adapt it for bash.
    – phk
    Commented Oct 12, 2015 at 12:43
  • @schily: I don't know what you mean with "coprocess" though.
    – phk
    Commented Oct 12, 2015 at 13:09
  • Well, the Korn Shell man page says read -p reads input from the current coprocess.
    – schily
    Commented Oct 12, 2015 at 14:15

5 Answers 5


Actually, interrupting bash's internal read seems to be a bit different to interrupting a command run by bash. Normally, when you enter trap, $? is set and you can preserve it and exit with the same value:

trap 'rc=$?; echo $rc SIGINT; exit $rc' INT
trap 'rc=$?; echo $rc EXIT; exit $rc' EXIT

If your script is interrupted when executing a command like sleep or even a builtin like wait, you will see

130 EXIT

and the exit code is 130. However, for read -p, it seems $? is 0 (on my version of bash 4.3.42 anyway).

The handling of signals during read might be work in progress, according to the changes file in my release... (/usr/share/doc/bash/CHANGES)

changes between this version, bash-4.3-alpha, and the previous version, bash-4.2-release.

  1. New Features in Bash

    r. When in Posix mode, `read' is interruptible by a trapped signal. After running the trap handler, read returns 128+signal and throws away any partially-read input.

  • OK, that's really odd. It's 130 on the interactive shell on bash 4.3.42 (under cygwin), 0 under the same shell when in a script and POSIX mode or not makes no difference. But then under dash and busybox it's always 1.
    – phk
    Commented Oct 12, 2015 at 13:28
  • I tried the POSIX mode with a trap that doesnt exit, and it restarts the read, as noted in the CHANGES file (added to my answer). So, it might be work in progress.
    – meuh
    Commented Oct 12, 2015 at 13:45
  • 1
    The exit code 130 is 100% non-portable. While the Bourne Shell uses 128 + signo as the exit code for signals, ksh93 uses 256 + signo. POSIX says: something above 128....
    – schily
    Commented Oct 12, 2015 at 14:22
  • @schily True, as noted in the thread I linked to in the original post (unix.stackexchange.com/questions/99112).
    – phk
    Commented Oct 12, 2015 at 14:23

Any usual signal exit code will be available in $? upon entry to the trap handler:

sig_handler() {
    exit_status=$?  # Eg 130 for SIGINT, 128 + (2 == SIGINT)
    echo "Doing signal-specific up"
    exit "$exit_status"
trap sig_handler INT HUP TERM QUIT

If there is a separate EXIT trap, you can use the same methodology: immediately keep the exit status passed from the signal handler (if there is one) clean-up, then return the saved exit status.

  • This applies to most shells? Very nice. local isn't POSIX though.
    – phk
    Commented Sep 30, 2018 at 18:41
  • 1
    Edited. I've not tested in other than {ba,z}sh. AFAIK, it's the best that can be done, regardless.
    – Tom Hale
    Commented Oct 1, 2018 at 6:54
  • This doesn't work in dash ($? is 1 whatever the received signal is).
    – MoonSweep
    Commented Mar 20, 2019 at 10:44

Just return some error code isn't enough to simulate an exit by SIGINT. I'm surprised that nobody mentioned this so far. Further reading: https://www.cons.org/cracauer/sigint.html

The proper way is:

    trap "cleanup;
          [ $sig  = EXIT ] && normal_exit_only_cleanup;
          [ $sig != EXIT ] && trap - $sig EXIT && kill -s $sig $$
         " $sig

This works with Bash, Dash and zsh. For further portability, you need to use numeric signal specifications (on the other hand, zsh expects a string parameter for the kill command...)

Also note the special treatment of the EXIT signal. This is due to some shells (namely Bash) executing traps on EXIT for any signal also (after possibly defined traps on that signal). The reset of the EXIT trap prevents that.

Executing code only on "normal" exit

The check [ $sig = EXIT ] allows for executing code only on normal (non-signaled) exit. However, all signals have to have traps that at last reset the trap on EXIT then; normal_exit_only_cleanup will also be called for the signals that don't. It will also be executed by an exit through set -e. This can be fixed by trapping on ERR (which is not supported by Dash) and adding a check [ $sig = ERR ] before the kill.

Simplified Bash-only version

On the other hand, this behavior means that in Bash you can simply do

trap cleanup EXIT

to just execute some cleanup code and retain the exit status.


  • Elaborate Bash's behavior of "EXIT traps everything"

  • Remove KILL signal, which can't be trapped

  • Remove SIG prefixes from signal names

  • Don't attempt to kill -s EXIT

  • Take set -e/ERR into account

  • 1
    There's no general requirement that a program that traps signals terminate in a way that preserves the fact that it received a signal. For instance a program may declare that sending SIGINT is the way to shut it down and it may decide to exit with 0 if it managed to terminate without error or another error code if it did not shutdown cleanly. Case in point: top. Run top; echo $? and then hit Ctrl-C. The status dumped on the screen will be 0.
    – Louis
    Commented Jul 22, 2019 at 17:53
  • 1
    Thank you for pointing that out. However, the poster specifically asked about keeping the exit code. Commented Aug 9, 2019 at 9:19
  • Having spent the last two days navigating a jungle of semi-working solutions with limited portability across various shells, and slowly groping towards what you've outlined here, I must say your answer is seriously underappreciated! And the linked article is great as well. Kudos and thanks :)
    – dlukes
    Commented Dec 9, 2021 at 23:49

All you need to do is change the EXIT handler inside your cleanup handler. Here's an example:

cleanup() {
    echo trapped exit
    trap 'exit 0' EXIT
trap cleanup EXIT
read -p 'If you ctrl-c me now my return code will be the default for SIGTERM. '
  • did you mean trap cleanup INT instead of trap cleanup EXIT?
    – Jeff Schaller
    Commented Oct 12, 2015 at 12:45
  • I think I meant EXIT. When exit is finally called, however that's done, we change the trap to exit with return 0. I don't believe signal handlers are recursive.
    – rocky
    Commented Oct 12, 2015 at 12:59
  • OK, in your example I am not trapping (SIG)INT at all which is actually what I want in order to do some cleaning up even if the user is exiting my interactive script through ctrl-c.
    – phk
    Commented Oct 12, 2015 at 13:06
  • @phk Ok. I think though you get the idea of how to force exit to return 0 or some other value which what I gathered was the problem. I assume you'll be able to adjust the code to put the trap EXIT setting inside your real cleanup handler which is called by SIGINT.
    – rocky
    Commented Oct 12, 2015 at 13:12
  • 1
    Doesn't work under zsh.
    – Tom Hale
    Commented Sep 29, 2018 at 17:04

Here's a simplified 2 liner approach from another question: https://stackoverflow.com/a/53714583/16780116

trap 'RC=${?}; kill 0' EXIT
trap 'exit ${RC:-}' INT TERM

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .