20

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):

#!/bin/bash
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. ' _

Output:

$ ./test.sh # doing ctrl-c for 1st read
If you ctrl-c me now my return code will be the default for SIGTERM.
$ echo $?
130
$ ./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 
EXIT
$ echo $?
1

(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.

Update:

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
phk
  • 5,953
  • 7
  • 42
  • 71
  • 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 Oct 12 '15 at 12:36
  • Ah yes, under POSIX it's of course without the SIG-prefix. – phk Oct 12 '15 at 12:40
  • Oops, but then "read -p" wouldn't be supported either, so I am going to adapt it for bash. – phk Oct 12 '15 at 12:43
  • @schily: I don't know what you mean with "coprocess" though. – phk Oct 12 '15 at 13:09
  • Well, the Korn Shell man page says read -p reads input from the current coprocess. – schily Oct 12 '15 at 14:15
  • Ah, nice parameter collision then. I was actually thinking of BusyBox when writing my code (and BB implements certains bashisms such as this I guess) which is why I used the /bin/sh shebang originally but I should've clarified, my bad. – phk Oct 12 '15 at 14:30
  • Given that read -p is already documented for ksh88 which predates bash this looks like a problem caused by bash. – schily Oct 12 '15 at 16:28

4 Answers4

13

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 SIGINT
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.

meuh
  • 51,383
  • 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 Oct 12 '15 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 Oct 12 '15 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 Oct 12 '15 at 14:22
  • @schily True, as noted in the thread I linked to in the original post (http://unix.stackexchange.com/questions/99112). – phk Oct 12 '15 at 14:23
8

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.

Tom Hale
  • 30,455
8

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:

for sig in EXIT ABRT HUP INT PIPE QUIT TERM; do
    trap "cleanup;
          [ $sig  = EXIT ] && normal_exit_only_cleanup;
          [ $sig != EXIT ] && trap - $sig EXIT && kill -s $sig $$
         " $sig
done

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.

edited

  • 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 Jul 22 '19 at 17:53
  • 1
    Thank you for pointing that out. However, the poster specifically asked about keeping the exit code. – philipp2100 Aug 09 '19 at 09: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 Dec 09 '21 at 23:49
4

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

#!/bin/bash
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. '
rocky
  • 1,998
  • did you mean trap cleanup INT instead of trap cleanup EXIT? – Jeff Schaller Oct 12 '15 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 Oct 12 '15 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 Oct 12 '15 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 Oct 12 '15 at 13:12
  • @rocky: My goal was to have a trap handler while not changing the exit code that I would normally have without the handler. Now I could simply return the exit code you would normally get but the problem was that I don't know the exact exit code in these cases (as seen in the thread I linked to and the post from meuh it's quite complicated). Your post was still helpful, I didn't think about redefining the trap handler dynamically. – phk Oct 12 '15 at 14:09
  • 1
    Doesn't work under zsh. – Tom Hale Sep 29 '18 at 17:04