4

I'm looking for a builtin or standard package that provides functionality that is similar or equivalent to stdlib's atexit() and bash' trap "..." EXIT.

It should catch termination due to any programatic way of ending execution, including all of the following:

  • naturally reached end of script execution
  • explicitly invoked exit
  • uncaught error
rtx13
  • 2,580
  • 1
  • 6
  • 22

2 Answers2

4

In most cases, all you need to do to intercept such terminations is to intercept the exit command.

rename exit real_exit
proc exit args {
    puts "EXITING"; flush stdout; # Flush because I'm paranoid...
    tailcall real_exit {*}$args
}

That will obviously work if you call it explicitly, but it also gets called if you just drop off the end of the script, signal end of file in an interactive session, or if your script has an error in it later and terminates with an error message. This is because the Tcl C API call, Tcl_Exit(), works by calling exit and, if that doesn't exit the process, directly does the exit itself.

Be careful with exit scripts BTW; errors in them are harder to debug than normal.


The cases where it doesn't work? Mainly where the interpreter itself has become unable to execute commands (perhaps because it has been deleted out from under itself) or where some signal closes down the interpreter (e.g., SIGINT isn't handled by default for various reasons).

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • Tcl has true exit handlers too, but they're only at the C API level because they get called even when no interpreter is operational. https://www.tcl-lang.org/man/tcl8.6/TclLib/Exit.htm – Donal Fellows Apr 06 '20 at 17:11
  • Thanks that works. I tried this approach earlier but must have made a mistake as it wasn't catching exits due to `error`. – rtx13 Apr 06 '20 at 19:50
0

A more-or-less complete atexit based on @Donal's answer:

proc atexit { procbody } {
    if { [catch {set oldbody [info body exit]}] } {
        rename exit builtin_exit
        set oldbody { builtin_exit $returnCode }
    }
    proc exit { {returnCode 0} } [subst -nocommands {
        apply [list [list {returnCode 0}] [list $procbody]] \$returnCode
        tailcall apply [list [list {returnCode 0}] [list $oldbody]] \$returnCode
    }]
}

Sample code for atexit-test.tcl:

#!/usr/bin/tclsh8.6

source atexit.tcl

atexit {
    puts "EXITING($returnCode)"; flush stdout; # Flush because I'm paranoid...
}

atexit {
    puts "done($returnCode)..."; flush stdout; # Flush because I'm paranoid...
}

atexit {
    puts "almost($returnCode)..."; flush stdout; # Flush because I'm paranoid...
}

{*}$argv

puts "fell through argv for implicit exit..."

... and terminal session:

$ ./atexit-test.tcl 
fell through argv for implicit exit...
almost(0)...
done(0)...
EXITING(0)
$ ./atexit-test.tcl exit
almost(0)...
done(0)...
EXITING(0)
$ ./atexit-test.tcl exit 5
almost(5)...
done(5)...
EXITING(5)
$ ./atexit-test.tcl error "unhandled exception"
unhandled exception
    while executing
"{*}$argv"
    (file "./atexit-test.tcl" line 17)
almost(1)...
done(1)...
EXITING(1)
$ 
rtx13
  • 2,580
  • 1
  • 6
  • 22