Updated 2006-07-25 11:58:30 by NEM

errx

(You may want to have a look at Debugging Aid For Production Code, too)

The idea of errx came to me as I was enerved by debugging puts. It resembles a bit of the BSD errx/warnx pair, although it combines both functionalities into one function. Furthermore it offers something like a debuggers bt , i.e. backtrace.

It is not yet finished, but I use it daily already, although the more fancy things (like the bt) are not yet fully finished or even nonexistant.

MSW

Make it simple, paste it here - code on a wiki page, with the ensuing discussions, is more attractive and pedagogical than a URL to download a tar.gz bundle... (RS ;-)

In fact I had exactly that in mind - errx itself is lean, but I am using it in a bundle with some other stuff - bgerror, and some other utility stuff which I've packed into ::Error ... gonna present errx here and offer the rest for download :)

And here we go. This is the factored out errx which I already use. I have cut out about anything which is not yet finished.
 # When this is sourced, set errchann and debugging if not set
 # to sane values.
 if {![info exists ::errchann]} { set ::errchann stderr }
 if {![info exists ::debugging]} {
    set ::debugging 0
 } elseif {!$::debugging} {
    interp alias {} errx {} list
 }

 # Used to provide the stack traces
 proc unstacker {{txt {}}} {
    if {[string length $txt]} {
        puts $::errchann $txt
    }
    uplevel {
        for {set lv [expr [info level] -1 ]} { $lv } { incr lv -1 } {
            puts $errchann\
		"================================================================================"
            puts $errchann " Level $lv, [info level $lv]."
            puts $errchann " Local variables:"
            uplevel \#$lv {
		foreach v [info locals] {
		    upvar #[info level] $v _v; puts -nonewline $::errchann "$v=$_v\t"
		}
	    }
            puts $errchann ""
        }
    }
 }

 proc errx { txt {flags {info debug}} } {
    global argv0 debugging errchann errorInfo lastargs

    if {![string length $errchann]} { return }

    if {![info exists debugging]} { set debugging 0 }

    # if !debugging immediate return when flags contain debug
    if {([set dbgpos [lsearch $flags debug]] != -1) && !$debugging} {
	return
    } else {
	set flags [lreplace $flags $dbgpos $dbgpos]
    }

    upvar _DEBUG _debug
    if {![info exists _debug]} {
	set _debug 0
    }
    if {([set dbgpos [lsearch $flags DEBUG]] != -1)
	&& !$_debug} {
	return
    } else {
	set flags [lreplace $flags $dbgpos $dbgpos]
    }

    if {[info level]==1}  { ;# caller is global!
        set caller "*global*"
        set verbose ""
    } else {
        set caller [lindex [info level -1] 0]
        set verbose "$argv0:\[<$caller>\] Called as: [info level -1]"
    }
    if {[set flp [lsearch $flags time]]!= -1} {
        set intro "$argv0@([clock format [clock seconds]]):\[<$caller>\]"
        set flags [lreplace $flags $flp $flp]
    } else {
        set intro "$argv0:\[<$caller>\]"
    }

    set txt [uplevel [list subst $txt]]

    switch -- $flags {
        sparse      {   puts $errchann "$txt" }
        ""          -
        debug       { ;# this shouldn't happen, but can if two debug flags are given.
	    puts $errchann "$intro:<DEBUG> $txt"
	}
        info        {   puts $errchann "$intro:<INFO> $txt" }
        warning     {   puts $errchann "$intro:<WARNING> $txt"}
        error       {   puts $errchann "$intro:<ERROR!> $txt\n$verbose" ; exit 1 }
        critical    {   puts $errchann "$intro: ** CRITICAL ERROR ! **"
	    if {[info exists errorInfo]} { puts $errchann " -- $errorInfo" }
	    if {[info exists errorCode]} { puts $errchann " -- $errorCode" }
	    unstacker
	    exit 1
	}
        stack       {   unstacker $txt }
        vars        {
            puts $errchann "$intro:<VarInfo> $txt"
	    uplevel {
		foreach v [info locals] {
		    upvar #[info level] $v _v; puts -nonewline $::errchann "$v=$_v\t"
		}
	    }
	    puts $errchann ""
	}
	default     {
	    errx "Invalid flags given to errx ($flags)!" error
	}
    }
    return
 }

Its strong points are mainly that you can switch messages on and off, and even use for non debugging, but warning or informational purposes.

If the variable _DEBUG is set in the calling environment, and set to 1 (or true), then the DEBUG token works. If the global variable debugging is set, then the debug token works. (works in the sense like "the debug modifier works", see the first and third example)

In general you'd use it like follows:
 # if {$_DEBUG} in calling environment, output <INFO> msg
 errx {local-var-I-want-to-know is $bla} {info DEBUG}

 # output a <WARNING> unconditionally
 errx {I just deleted my harddisk!} warning

 # if {$::debugging}, output a <debug> msg
 errx {Hmm, environment doesn't seem sane...} debug

 # Output a error along with stack backtrace, inspection
 # of local variables up the stack and a timestamp,
 # exit the application.
 errx {YIKES! THE ALIENS ARE COMING!} {critical time}

 # Kill the application with your error message
 errx {hmm, something went wildly wrong.} error

What I like about it is that it displays the caller and the script for which it works, normally with 'puts' you do all the same all the time:
 puts stderr "this-proc, that-var is $bla @ [clock format [clock seconds]"

Furthermore, when you switch your code to use a global routine like that, can easily say something like:
 set ::debugging 1
 set ::errchann [open debug.log {WRONLY CREAT TRUNC}]

and collect all information from it at a central place...

Once I finished errx to the like I want it, I'll put up the tar.gz I mentioned earlier. Included in this Error module is a facility for bgerror handling (installing/reinstalling of handlers etc.), setting of debug - levels (offers control of verbosity on a global level, too), Switching of errchann with reinstalling the old one, a predefined bgerror for use with event-driven tcl applications, and concise documentation.

Things errx is going to learn still:

  • norecord, record
  • replay
  • rdr
  • ml or multiline
  • filtered stack trace with help of errorInfo

- these are to be combined, too ... if you have further suggestions, well, this is a wiki :) Go ahead.

If you wonder what for all that junk, the ultimate goal is to create something you can distribute with your application and which hides debug messages from the user. If he has a serious problem, he can send a problem report which included all kinds of debug messages collected internally, along with stack traces, local variables etc. etc.

For the 'filtered stack trace', this is what I mean:
 % proc x1 {} {
    puts bla
    puts blah
    x2
    puts blabla
 }
 % proc x2 {} {
    puts banzai
    error ayaken
    puts dead
 }
 % catch x1 zack

 % puts $zack
 ayaken

At this place, in errorInfo a nice stacktrace is in. It contains line numbers, too, which we can re-transform into code by the following:
 % lindex [split [info body <proc>] "\n"] [expr <lineno in errInf> - 1]

Or come up with a bit of environment in the calling function easily with
 % lrange [split [info body <proc>] "\n"] [expr <lineno in errInf> -3] [expr <lineno in errInf> +2]

I like the idea, gonna put it into the Error module I'll end up with...

4/9/2002: Nice! The one point I'd like to raise, is that the arg expansion of Tcl makes it unattractive to leave lots of errx calls in (just like various flavors of "assert"). Example:
	errx "time needed for 1000 calls is [time {mycall arg} 1000]" debug

That statement is going to take time even when debugging is turned off.

Slightly less convenient, but a way to solve this, is to use uplevel inside errx so calls can be rewritten as:
	errx {time needed for 1000 calls is [time {mycall arg} 1000]} debug

It ain't perfect, but at least this way one can leave all the calls in and have them take relatively little time (even less if errx gets redefined to "prox errx args {}" when pressed for time). Would this approach be an idea worth considering for errx? -jcw

05/09/2002 CET :)

Yes, you got a point there. In fact, I have not used errx's arguments up to now to carry "expensive" operations like the one you mentioned, although I do think your point is valid. I tried to toy around with something like
 interp alias {} errx {} \;\#

to turn it off when there is no debugging, but that didn't work. -MSW

RS: interp alias maps commands to other commands, and neither semicolon nor the "pound" (#) are commands. An almost do-nothing command would be list:
 interp alias {} errx {} list

It returns (to nobody, if it's not the last command in a proc) its arguments.

MSW: K, you're right. But jcw's point still would be valid if I mapped it to list... On the other hand, if it had been able to map it to a comment, the arguments to errx would not have been evaluated.

...And I just changed errx to be used like jcw suggested.

I wonder if I should put the implementations of the other mentioned/planned features of errx on this page, it's so long already :)

Oh, RS, your suggestion in conjunction with the solution jcw has suggested, makes calls to errx if it's alias'd to list avoids all that cost - which is why I have changed the initialization a bit, too.

NEM Note that using list will still cause some runtime overhead. Better to use "proc errx args {}" as jcw suggested -- the Tcl bytecode compiler will then optimise the calls away completely (or, near enough, anyway).

Category Debugging