When running commands interactively at the tclsh command line, is there a way to truncate how much of a return value gets printed to stdout?
For example, this will take a very long time because the return value will print to stdout.
tclsh> set a [lrepeat 500000000 x]
I know I can add a dummy command in the same line, but this is an ad hoc solution. Is there some I could set in my ~/.tclshrc to truncate stdout to a finite length?
tclsh> set a [lrepeat 500000000 x] ; puts ""
Maybe this is an XY-problem (as turning off or swallowing prints to stdout seems to satisfy the OP), but the actual question was:
Is there some I could set in my ~/.tclshrc to truncate stdout to a
finite length?
You can use an interceptor on stdout (and/ or, stderr) to cap strings to a default limit:
oo::class create capped {
variable max
constructor {m} {
set max $m
}
method initialize {handle mode} {
if {$mode ne "write"} {error "can't handle reading"}
return {finalize initialize write}
}
method finalize {handle} {
# NOOP
}
method write {handle bytes} {
if {[string length $bytes] > $max} {
set enc [encoding system]
set str [encoding convertfrom $enc $bytes]
set newStr [string range $str 0 $max-1]
if {[string index $str end] eq "\n"} {
append newStr "\n"
}
set bytes [encoding convertto $enc $newStr]
}
return $bytes
}
}
Using chan push and chan pop you may turn on/off capping to, e.g., 30 characters:
% chan push stdout [capped new 30]
serial1
% puts [string repeat € 35]
€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
% chan pop stdout
% puts [string repeat € 35]
€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€
Some remarks:
You can use an object, a namespace, or a proc offering the required interface of channel interceptors (initialize, write, ...); I prefer objects.
Ad write: You want to cap based on a character-based limit, not a byte-level one. However, write receives a string of bytes, not a string of characters. So, you need to be careful when enforcing the limit (back-transform the byte string into a char string, and vice versa, using encoding convertfrom and encoding convertto).
Similar, whether certain values of max might not be a good choice or the value range should be restricted. E.g., a max of 1 or 0 will turn off the basic REPL (the prompt % ), effectively.
As for tclshrc: You may want place the interceptor definition and chan push call therein, to enable capping per default?
tclsh is a REPL, and the "P" there is what you're seeing. Without digging into the source, I don't know that there's a simple way to accomplish exactly what you're asking.
If I remember to do it, the list command is useful to provide no output
set a [lrepeat 500000000 x]; list
or perhaps something informative
set a [lrepeat 500000000 x]; llength $a
If you want to get programmy:
proc i {val} {set ::tcl_interactive $val}
Then do i off or i 0 or i false to turn off interactivity and then execute the commands with large results. Going non-interactive silences the printing of command results, but it also turns off the prompt which could be confusing. Restore interactivity with i on or i 1 or i true
Related
In Tcl, if I evaluate a command, and my expression failed to store the result in a variable, other than reevaluating the command, is there a way to retrieve the last command's return value in a TCL interpreter? For example, in some lisps, the variable * is bound to the result of the last evaluation, is there something like that in TCL?
The Tcl interpreter loop — which is pretty simple minded, with very few hidden features — doesn't save the last result value for you; it assumes that if you want to save that for later, you'll do so manually (or you'll repeat the command and get the result anew so you can save it then).
If you want to save a value for later, use set. You can use * as the name of the variable (it is legal) but it's annoying in practice because the $var syntax shortcut doesn't work with it and instead you'd need to do:
% set * [expr {2**2**2**2}]; # Or whatever...
65536
% puts "the value was ${*}... this time!"
the value was 65536... this time!
% puts "an alternative is to do [set *]"
an alternative is to do 65536
This will probably be a bit annoying when typing; use a name like it instead.
% set it [expr {2**2**2**2}]; # Or whatever...
65536
% puts [string length [expr {123 << $it}]]
19731
(That last number has rather a lot of digits in it, more than I expected…)
For example, in some lisps, the variable * is bound to the result of
the last evaluation, is there something like that in TCL?
It depends, but in the non-error case, catch can be used to, well, catch the result of the last successful evaluation within the enclosed script:
proc foo {} {return FOO}
proc bar {} {return BAR}
if {![catch {
if {rand() < 0.5} {foo} else {bar}
} * opts]} {
# handle result of last cmd evaluation
puts ${*}
} else {
# handle an error
puts [dict get $opts -errorinfo]
}
If you happen to know the command names in advance, command traces might be another option.
This is the code in TCL that is meant to produce factorial of a number given as parameter by the user.
if {$argc !=1}{
puts stderr "Error! ns called with wrong number of arguments! ($argc)"
exit 1
} else
set f [lindex $argv 0]
proc Factorial {x}{
for {set result 1} {$x>1}{set x [expr $x - 1]}{
set result [expr $result * $x]
}
return $result
}
set res [Factorial $f]
puts "Factorial of $f is $res"
There is a similar SO question, but it does not appear to directly address my problem. I have double-checked the code for syntax errors, but it does not compile successfully in Cygwin via tclsh producing the error:
$ tclsh ext1-1.tcl
extra characters after close-brace
while executing
"if {$argc !=1}{
puts stderr "Error! ns called with wrong number of arguments! ($argc)"
exit 1
} else
set f [lindex $argv 0]
proc Factorial {x}{..."
(file "ext1-1.tcl" line 3)
TCL Code from: NS Simulator for Beginners, Sophia-Antipolis, 2003-2004
Tcl is a little bit more sensitive about whitespace than most languages (though not as much as, say, Python). For instance, you can't add unescaped newlines except between commands as command separators. Another set of rules are that 1) every command must be written in the same manner as a proper list (where the elements are separated by whitespace) and 2) a command invocation must have exactly the number of arguments that the command definition has specified.
Since the invocation must look like a proper list, code like
... {$x>1}{incr x -1} ...
won't work: a list element that starts with an open brace must end with a matching close brace, and there can't be any text immediately following the close brace that matches the initial open brace. (This sounds more complicated than it is, really.)
The number-of-arguments requirement means that
for {set result 1} {$x>1}{incr x -1}{
set result [expr $result * $x]
}
won't work because the for command expects four arguments (start test next body) and it's only getting two, start and a mashup of the rest of other three (and actually not even that, since the mashup is illegal).
To make this work, the arguments need to be separated:
for {set result 1} {$x>1} {incr x -1} {
set result [expr {$result * $x}]
}
Putting in spaces (or tabs, if you want) makes the arguments legal and correct in number.
having issues trying to debug this 'extra characters after close-brace' error. Error message points to my proc line ... I just can't see it for 2 days!
# {{{ MAIN PROGRAM
proc MAIN_PROGRAM { INPUT_GDS_OASIS_FILE L CELL_LIST_FILE } {
if { [file exists $CELL_LIST_FILE] == 0 } {
set celllist [$L cells]
} else {
set fp [open $CELL_LIST_FILE r]
set file_data [read $fp]
close $fp
set celllist [split $file_data "\n"]
set totalcells [expr [llength $celllist] - 1]
}
set counter 0
foreach cell $celllist {
set counter [expr {$counter + 1}]
set value [string length $cell]
set value3 [regexp {\$} $cell]
if { $value > 0 && $value2 == 0 && $value3 == 0 } {
# EXTRACT BOUNDRARY SIZE FIRST
puts "INFO -- READING Num : $counter/$totalcells -- $cell ..."
ONEIP_EXTRACT_BOUNDARY_SIZE $cell $L "IP_SIZE/$cell.txt"
exec gzip -f "IP_SIZE/$cell.txt"
}
}
# }}}
}
# }}}
This seems to be an unfortunate case of using braces in comments. The Tcl parser looks at braces before comments (http://tcl.tk/man/tcl8.5/TclCmd/Tcl.htm). It is a problem if putting braces in comments causes a mismatched number of open/close braces.
Try using a different commenting style, and remove the "{{{" and "}}}" from your comments.
I'm pretty sure that this is down to braces in comments within the proc body.
The wiki page here has a good explaination. In short a Tcl comment isn't like a comment most other languages and having unmatched braces in them leads to all
sorts of issues.
So the braces in the #}}} just before the end of the proc are probably the problem.
Tcl requires procedure bodies to be brace-balanced, even within comments.
OK, that's a total lie. Tcl really requires brace-quoted strings to be brace-balanced (Tcl's brace-quoted strings are just like single-quoted strings in bash, except they nest). The proc command just interprets its third argument as a script (used to define the procedure body) and it's very common to use brace-quoted strings for that sort of thing. This is a feature of Tcl's general syntax, and is why Tcl is very good indeed at handling things like DSLs.
You could instead do this:
proc brace-demo args "puts hi; # {{{"
brace-demo do it yeah
and that will work fine. Totally legal Tcl, and has a comment in a procedure body with unbalanced braces. It just happens that for virtually any real procedure, putting in all the required backslashes to stop interpretation of variable and command substitutions too soon is a total bear. Everyone uses braces for simplicity, and so has to balance them.
It's hardly ever a problem except occasionally for comments.
I'm relatively new in TCL, in TCL prompt, when we invoke a proc with some return value, the proc's return value is echoed back by tcl. Is there a way to stop it (without affecting puts or similar functionality) as an example
bash$ tclsh
% proc a {} { puts "hello"; return 34; }
% a
hello
34
%
Now how do i suppress the 34 coming to the screen? Any help is appreciated.
Update:
Actually the proc is a part of another tool, earlier it did not have any return value, but now conditionally it can return a value.
it can be called from a script and there won't be any problem (as Bryan pointed out). and it can be called from interactive prompt, then after all the necessary outputs, the return value is getting printed unnecessarily.
So 1) I don't have the facility of changing a user's tclshrc 2) existing scripts should continue to work.
And it seems strange that every time the proc is called, after all the necessary outputs, a number gets printed. To a user, this is a needless information unless he has caught the value and wants to do something. So i wanted the value to be delivered to user, but without getting printed to prompt/UI (hope i'm clear )
The interactive shell code in tclsh and wish will print any non-empty result. To get nothing printed, you have to have the last command on the “line” produce an empty result. But which command to use?
Many commands will produce an empty result:
if 1 {}
subst ""
format ""
However, the shortest is probably:
list
Thus, you could write your code like:
a;list
Of course, this only really becomes useful when your command actually produces a large result that you don't want to see. In those cases, I often find that it is most useful to use something that measures the size of the result, such as:
set tmp [something_which_produces a_gigantic result]; string length $tmp
The most useful commands I find for that are string length, llength and dict size.
If you absolutely must not print the result of the command, you have to write your own interactive loop. There are two ways to do this, depending on whether you are running inside the event loop or not:
Without the event loop
This simplistic version just checks to see if the command name is in what the user typed. It's probably not a good idea to arbitrarily throw away results otherwise!
set accum ""
while {[gets stdin line] >= 0} {
append accum $line "\n"
if {[info complete $accum]} {
if {[catch $accum msg]} {
puts stderr $msg
} elseif {$msg ne "" && ![string match *TheSpecialCommand* $accum]} {
puts $msg
}
set accum ""
}
}
With the event loop
This is just handling the blocking IO case; that's the correct thing when input is from a cooked terminal (i.e., the default)
fileevent stdin readable handleInput
set accum ""
proc handleInput {} {
global accum
if {[gets stdin line] < 0} {
exit; # Or whatever
}
append accum $line "\n"
if {[info complete $accum]} {
if {[catch {uplevel "#0" $accum} msg]} {
puts stderr $msg
} elseif {$msg ne "" && ![string match *TheSpecialCommand* $accum]} {
puts $msg
}
set accum ""
}
}
vwait forever; # Assuming you're not in wish or have some other event loop...
How to detect the command is being executed
The code above uses ![string match *TheSpecialCommand* $accum] to decide whether to throw away the command results, but this is very ugly. A more elegant approach that leverages Tcl's own built-in hooks is to use an execution trace to detect whether the command has been called (I'll just show the non-event-loop version here, for brevity). The other advantage of this is that it is simple to extend to suppressing the output from multiple commands: just add the trace to each of them.
trace add execution TheSpecialCommand enter SuppressOutput
proc SuppressOutput args {
# Important; do not suppress when it is called inside another command
if {[info level] == 1} {
set ::SuppressTheOutput 1
}
}
# Mostly very similar from here on
set accum ""
while {[gets stdin line] >= 0} {
append accum $line "\n"
if {[info complete $accum]} {
set SuppressTheOutput 0; # <<<<<< Note this!
if {[catch $accum msg]} {
puts stderr $msg
} elseif {$msg ne "" && !$SuppressTheOutput} { # <<<<<< Note this!
puts $msg
}
set accum ""
}
}
To be clear, I wouldn't ever do this in my own code! I'd just suppress the output manually if it mattered.
You could make an empty procedure in .tclshrc...
proc void {} {}
...and when you don't need a return value, end the line with ;void.
Use tcl_interactive variable to enable the return of of the value, although I'm not sure where this would be useful...
proc a {} {
puts "hello"
if { [info exist tcl_interactive] } {
return {};
} else {
return 34;
}
}
Is there a way to ignore backspaces when performing a flush in tcl to capture user input?
I am performing a function where I capture the user input in a variable to be used in another command at a later time. So I perform the following function.
puts -nonewline "What is the username? "
flush stdout
set usrnm [gets stdin]
So let's say using that command as long as I don't use a backspace everything works the way I expect it however if I do use a backspace a "\x7F" is added as a character. Is there a way for the backspace to not be treated as a character?
That seems to depend on your terminal; when I try that code with these key sequences:
BackspaceabcReturn
abcBackspacedReturn
Then I get a length 3 string (measured via string length) in the usrnm variable in both cases. This is what I'd expect when the terminal is properly in cooked mode (the usual default). Since a \x7f is probably not a valid character in a user name anyway, I'd guess that you could filter it out:
set usrnm [string map {\x7f ""} $usrnm]
The only way to be absolutely sure that the character isn't there is to put the terminal in to raw mode (and probably no-echo too) and do all the character input processing yourself. That's a huge amount of work relative to the size of problem; a post-filter seems more sensible to me (and I still wonder what's up with your terminal).
[EDIT]: To put your terminal back into cooked mode, do:
exec stty -raw <#stdin
I just ran into this recently and I wrote a procedure to handle the char 127 character (backspace). If any other input cleansing needs to happen you can do it here too, such as removing special characters. I have a feeling this can be more elegant but it does work.
proc cleanInput {str} {
set return ""
for {set i 0} {$i < [string length $str]} {incr i} {
set char [string index $str $i]
set asc [scan $char %c]
if {$asc == 127} { #backspace
if {[string length $return] > 0} {
set return [string range $return 0 [expr "[string length $return] - 2"]]
}
} else {
append return $char
}
}
return $return
}