How to control the character length of strings in tcl errors? - tcl

Long strings which appear in tcl error messages are elided with ... after 150 characters:
proc helloWorld {a} {
throw
}
helloWorld "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
Error:
invalid command name "throw"
while executing "throw "
(procedure "helloWorld" line 2)
invoked from within "helloWorld "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff..."
(file "mytcl" line 6)
Is there a way to change this so that more characters are shown?
In my large commercial tcl application, customers use file paths that are more than 150 characters long. We see 100 characters for a repository name, plus 100 characters for a location within the repository. Since only 150 characters appear, this means the "useful" part of the filename is not displayed in error messages. We have suggested to the customer that they could shorten the path using a symlink, but they do not accept this.

You can install your own background error handler.
You can log the error, display it how you want, etc.
interp bgerror {} ::bgerrhandler
# give err and res whatever names that make sense to you
proc ::bgerrhandler { err res } {
# do stuff
return
}

The problem that you've got is that the line-shortening is applied during the construction of the error trace message. It's already long gone by the time you get to see it. What you need instead is to install a custom background error handler using the new API that picks critical things out of the error stack. (The old API using a simple bgerror command is unsuitable for this task as it hides the result option dictionary for backward compatibility reasons.)
proc MyErrorHandler {msg opts} {
if {[dict get $opts -code] == 1} {
# Actually an error!
puts "ERROR: $msg"
foreach {op details} [dict get $opts -errorstack] {
puts stderr "$op :> $details"
}
return
}
# Transfer to standard implementation; tailcall recommended for this from 8.6 on
::tcl::Bgerror $msg $opts
}
interp bgerror {} MyErrorHandler
Here's an example of this working:
# Error generation code
proc foo {x} {throw BOOM $x}
proc bar {y} {foo $y$y$y$y}
proc grill {} {bar skdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsd}
# Now actually do things in the background
after idle grill; after 50 set z 1; vwait z
That will print this message:
ERROR: skdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsd
INNER :> returnImm skdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsd {-errorcode BOOM}
CALL :> foo skdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsdskdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsd
CALL :> bar skdfhglsdjklfgjhsdlfgjksdlkfhgklsdhfjklghskldfsldfkjghlsd
CALL :> grill
The INNER indicates what bytecode opcode actually threw, and then there's a sequence of CALLs to give the exact arguments lists that lead to that point.

Related

Tcl:Is there a way to show which line in tcl throw an error?

When I source a tcl file, error will be report when run to a unknown command.
Tcl> source run.tcl
$inside tcl file$$> setup_desgin
Design setup...
Done
$inside tcl file$$> my_prove
Info: proving started
Info: ....
Info: ....
$inside tcl file$$> ::unknown my_pro
invalid command name "my_pro"
Is there a way to show the line number of the error line in tcl file as below?
Tcl> source run.tcl
$inside tcl file$$> setup_desgin
Design setup...
Done
$inside tcl file$$> my_prove
Info: proving started
Info: ....
Info: ....
$inside tcl file$$> ::unknown my_pro
invalid command name "my_pro" (run.tcl:5)
We want this because we may have a very big run.tcl with minions of line.
It's actually quite difficult to get this information in general. The accurate line number information is only available at present when the code is on the execution stack. Fortunately, you can find out what's going on with a leave-step trace (intercepting unknown would only work for unknown commands; tracing lets all errors be caught). In the code below, I've put it on eval, but in a more practical example you'd put it on source or something like that.
set errorlineSet 0
proc LEAVESTEP {cmd code result op args} {
global errorline errorlineSet
if {$code == 1} {
if {!$errorlineSet} {
set errorline [dict get [info frame -4] line]
}
set errorlineSet 1
} else {
set errorlineSet 0
}
}
try {
trace add execution eval leavestep LEAVESTEP
eval {
sdfgsldfjg; # This is line 17
}
} on error {msg opt} {
puts "ERROR: $msg (line: $errorline) (local line: [dict get $opt -errorline])"
} finally {
trace remove execution eval leavestep LEAVESTEP
}
When I save all that to a file and run it, it prints this:
ERROR: invalid command name "sdfgsldfjg" (line: 17) (local line: 3)
As you can see, there is also the -errorline key in the result option dictionary, but that's mapped to 3 in this case and that's rather misleading! It uses that value because of backward compatibility, but I'm not convinced that it is all that helpful.
You can redefine the unknown command and use the info frame command in it to get the location. Something like:
# Save the default unknown if you want
rename ::unknown ::original_unknown
proc ::unknown {name args} {
set caller [info frame -1]
dict with caller {
switch $type {
source {
puts stderr "invalid command name \"$name\" (in file $file:$line)"
}
proc {
if {[info exists lambda]} {
puts stderr "invalid command name \"$name\" (in lambda $lambda:$line)"
} else {
puts stderr "invalid command name \"$name\" (in proc $proc:$line)"
}
}
eval {
puts stderr "invalid command name \"$name\" (in eval {$cmd}:$line)"
}
precompiled {
puts stderr "Invalid command name \"$name\""
}
}
}
}
Example usage:
% source my_unknown.tcl
% source bad.tcl
invalid command name "put" (in file /home/shawn/src/bad.tcl:3)

Why can't I access errorInfo and errorCode

I have the following code:
$ cat ~/tmp/2.tcl
set zero 0
proc p1 {} {
if {[catch {expr 1/$zero} err]} {
puts "errorCode=$errorCode"
puts "errorInfo=$errorInfo"
}
}
p1
When I source it, I get error accessing errorCode:
$ tclsh ~/tmp/2.tcl
can't read "errorCode": no such variable
while executing
"puts "errorCode=$errorCode""
(procedure "p1" line 3)
invoked from within
"p1"
(file "~/tmp/2.tcl" line 9)
I tried changing to $::errorCode, but did not help.
Can you see what is wrong?
The errorInfo and errorCode variables are globals. You should either use the global command to bring them into scope or use their fully-qualified names (i.e., precede with ::).
It might be easier to pick the information out of the result options dictionary (a new feature in 8.5).
Starting from Tcl 8.5 [catch] doesn't set the errorCode and errorInfo global variables. (As Donal has pointed out, it still does, so they can be accessed as $::errorCode and $::errorInfo). And in addition it puts their values into a dictionary which name is to be specified as the third argument. The following code
#!/usr/bin/tclsh
set zero 0
proc p1 {} {
if {[catch {expr 1/$zero} err opts] == 1} {
puts "errorCode=[dict get $opts -errorcode]"
puts "errorInfo=[dict get $opts -errorinfo]"
}
}
p1
prints
errorCode=NONE
errorInfo=can't read "zero": no such variable
while executing
"expr 1/$zero"
in Tcl 8.5.19, and
errorCode=TCL READ VARNAME
errorInfo=can't read "zero": no such variable
while executing
"expr 1/$zero"
in Tcl8.6.6.
You'd probably want to use $::zero in the division after which the result would be
errorCode=ARITH DIVZERO {divide by zero}
errorInfo=divide by zero
while executing
"expr 1/$::zero"

tcl/tk: avoid error message about remove non existing file

In my Tcl/Tk script, there is one step to remove some txt file.
I use:
exec rm file1.txt
But if the file dose not exist, then error message will come up which will block the script usage.
What I want to do is remove the file if it exist, and if it does not exist, to skip the error.
Is there a good way of doing this?
Ok, I find the answer: file exists filename works well for this case.
You could use
file delete file1.txt
where trying to delete a non-existent file is not considered an error.
How to avoid having an error stop your program.
The "0th" solution is to use commands that don't raise errors. such as glob -nocomplain instead of just glob, or in this case file delete file1.txt as suggested by timrau.
In some cases it's impossible to prevent errors from being raised. In those cases you can choose from several strategies. Assume you need to call mycmd, and it might raise errors.
# Tcl 8.6
try mycmd on error {} {}
# Tcl 8.4 or later
catch mycmd
This invocation quietly intercepts the error and lets your program continue. This is perfectly acceptable if the error isn't important, e.g. when you attempt to discard a variable that might not exist (catch {unset myvar}).
You might want to take some action when an error is raised, such as reporting it to yourself (as an error message on stderr or in a message box, or in a log of some kind) or by dealing with the error somehow.
try mycmd on error msg {puts stderr "There was a problem: $msg"}
if {[catch mycmd msg]} {
puts stderr "There was a problem: $msg"
}
You might want to take some action only if there was no error:
try {
mycmd
} on ok res {
puts "mycmd returned $res"
} on error msg {
puts stderr "There was a problem: $msg"
}
if {[catch mycmd res]} {
puts stderr "There was a problem: $res"
} else {
puts "mycmd returned $res"
}
For instance, this invocation returns the contents of a file, or the empty string if the file doesn't exist. It makes sure that the channel is closed and the variable holding the channel identifier are destroyed in either case:
set txt [try {
open $filename
} on ok f {
chan read $f
} on error msg {
puts stderr $msg
} finally {
catch {chan close $f}
catch {unset f}
}]
Documentation: catch, chan, file, glob, if, open, puts, set, try

TCL obtain the proc name in which I am

How to know what is the name of the proc in which I am. I mean I need this:
proc nameOfTheProc {} {
#a lot of code here
puts "ERROR: You are using 'nameOfTheProc' proc wrongly"
}
so I want to obtain "nameOfTheProc" but not hard-code. So that when someone will change the proc name it will still work properly.
You can use the info level command for your issue:
proc nameOfTheProc {} {
#a lot of code here
puts "ERROR: You are using '[lindex [info level 0] 0]' proc wrongly"
puts "INFO: You specified the arguments: '[lrange [info level [info level]] 1 end]'"
}
With the inner info level you will get the level of the procedure call depth you are currently in. The outer one will return the name of the procedure itself.
The correct idiomatic way to achieve what's implied in your question is to use return -code error $message like this:
proc nameOfTheProc {} {
#a lot of code here
return -code error "Wrong sequence of blorbs passed"
}
This way your procedure will behave exactly in a way stock Tcl commands do when they're not satisfied with what they've been called with: it would cause an error at the call site.
If your running Tcl 8.5 or later the info frame command will return a dict rather than a list. So modify the code as follows:
proc nameOfTheProc {} {
puts "This is [dict get [info frame [info frame]] proc]"
}

How to change Tcl error prefix?

The Tcl error command writes the specified message to stderr adding to it a prefix "Error: ".
Is it possible to change that prefix with something else like "ERROR: " or "MyError: " ?
The error command itself does not write anything to anywhere other than the Tcl result. What it actually does is throw an exception (of type “error”; TCL_ERROR at the C API level). The code that prefixes it with “Error: ” is just the standard fallback handler, but you can use your own by having your main script use catch to trap any result and decide what to do with it (or try … from 8.6 onwards, which is easier to use when trapping specific problems).
if {[catch {source realscript.tcl} msg]} {
# Caught an error!
puts stderr "Oh noes! Teh errorz iz atakkin! $msg"
puts stderr $::errorInfo
exit 1
}
OK, if you're using 8.5 then a slightly better way to write that is:
if {[catch {source realscript.tcl} msg resultdict]} {
# Caught an error!
puts stderr "Oh noes! Teh errorz iz atakkin! $msg"
puts stderr [dict get $resultdict -errorinfo]
exit 1
}
There's lots of other interesting stuff in the result dictionary, which you might or might not be interested in. Have a poke around.