Replace several lines of commands with a single variable in tcl - tcl

I know I have been asking a lot of questions but I'm still learning tcl and I haven't found anything that similar to this issue anywhere so far. Is it at all possible to replace a set f commands in tcl with one variable function0 for example?
I want to be able to replace the following code;
set f [listFromFile $path1]
set f [lsort -unique $f]
set f [lsearch -all -inline $f "test_*"]
set f [regsub -all {,} $f "" ]
set len [llength $f]
set cnt 0
with a variable function0 because this same code appears numerous times within the script. I should mention it appears both in a proc and not in a proc
The above code relates to similar script as
while {$cnt < $len} {
puts [lindex $f $cnt]
incr cnt
after 25; #not needed, but for viewing purposes
}

Variables are for storing values. To hide away (encapsulate) some lines of code you need a command procedure, which you define using the proc command.
You wanted to hide away the following lines
set f [listFromFile $path1]
set f [lsort -unique $f]
set f [lsearch -all -inline $f "test_*"]
set f [regsub -all {,} $f "" ]
set len [llength $f]
set cnt 0
to be able to just invoke for instance function0 $path1 and have all those calculations made in one fell swoop. Further, you wanted to use the result of calling the procedure in code like this:
while {$cnt < $len} {
puts [lindex $f $cnt]
# ...
Which means you want function0 to produce three different values, stored in cnt, len, and f. There are several ways to have a command procedure return multiple values, but the cleanest solution here is to make it return a single value; the list that you want to print. The value in len can be calculated from that list with a single command, and the initialization of cnt is better performed outside the command procedure. What you get is this:
proc function0 path {
set f [listFromFile $path]
set f [lsort -unique $f]
set f [lsearch -all -inline $f test_*]
set f [regsub -all , $f {}]
return $f
}
which you can use like this:
set f [function0 $path1]
set len [llength $f]
set cnt 0
while {$cnt < $len} {
puts [lindex $f $cnt]
incr cnt
after 25; #not needed, but for viewing purposes
}
or like this:
set f [function0 $path1]
set len [llength $f]
for {set cnt 0} {$cnt < $len} {incr cnt} {
puts [lindex $f $cnt]
after 25; #not needed, but for viewing purposes
}
or like this:
set f [function0 $path1]
foreach item $f {
puts $item
after 25; #not needed, but for viewing purposes
}
This is why I didn't bother to create a procedure returning three values: you only really needed one.
glenn jackman makes a very good point (or two points, actually) in another answer about the use of regsub. For completeness, I will repeat it here.
Tcl is a bit confusing because it usually allows string operations (like string substitution) on data structures that aren't formally strings. This makes the language very powerful and expressive, but also means that newbies do not always get the kick in the shins that a regular type system would give them.
In this case you created a list structure inside listFromFile by reading a string from a file and then using split on it. From that point on it's a list and you should only perform list operations on it. If you wanted to take out all commas in your data you should either perform that operation on each item in the list, or else perform the operation inside listFromFile, before splitting the text.
String operations on lists will work, but sometimes the result will be garbled, so mixing them should be avoided. The other good point was that in this case string map is preferable to regsub, if nothing else it makes the code a bit clearer.
Documentation: for, foreach, lindex, llength, lsearch, lsort, proc, puts, regsub, set, split, string, while

(more of a comment than an answer, but I want the formatting)
One thing to be aware of: $f holds a list, then you use the string command regsub on it, then you treat the result of regsub as a list again.
Use list commands with list values. I'd replace the regsub command with
set f [lmap elem $f {string map {"," ""} $elem} ]
for Tcl version 8.5 or earlier, you could do this:
for {set i 0} {$i < [llength $f]} {incr i} {
lset f $i [string map {, ""} [lindex $f $i]]
}

Related

Is there a simple way to parse a line of Tcl into its command and its arguments (not just splitting by whitespace)

Suppose I have a string which is also a Tcl command.
set line {lsort -unique [list a b c a]}
How can I convert this string into a list equivalent to this?
{
{lsort}
{-unique}
{[list a b c a]}
}
Because of whitespace inside the square brackets, I can't just use lindex.
For example:
> lindex $line 2
--> [list
The reason I'm asking is because I have a large Tcl script that I want to parse and re-write. I would like certain lines in the re-written script to have swapped argument order or some numerical arguments scaled by a factor.
I know I could parse the string character by character, keeping track of {}, [], and " characters, but this feels like re-inventing something that might already exist. I've been looking at the info and interp commands but couldn't find anything there.
I used info complete successfully in this proc.
proc command_to_list {command} {
# split by whitespace
set words [regexp -all -inline {\S+} $command]
set spaces [regexp -all -inline {\s+} $command]
set output_list [list]
set buffer ""
foreach word $words space $spaces {
append buffer $word
if {[info complete $buffer]} {
lappend output_list $buffer
set buffer ""
} else {
append buffer $space
}
}
return $output_list
}
This proc will group whitespace separated 'words' until they have no unmatched curlies, double quotes, or square brackets. Whitespace is preserved inside of matching pairs of curlies, double quotes or square brackets.
> set command {foreach {k v} [list k1 v1 k2 v2] {puts "$k $v"}}
> foreach word [command_to_list $command] {puts $word}
foreach
{k v}
[list k1 v1 k2 v2]
{puts "$k $v"}

How to pass integers to tcl procedure in vmd

I am new in tcl programming and I need to write a script for vmd that calculates two distances between two couples of atoms and print them in an output file. I do not understand why measure can not take atom_1, etc. This is my script and thank you for your help
proc distance {distance_1 atom_1 atom_2 distance_2 atom_3 atom_4 output} {
set outfile [open $output w]
puts $outfile "frame, $distance_1, $distance_2"
set nf [molinfo top get numframes]
for {set i 0} {$i < $nf} {incr i} {
set d1 [measure bond {$atom_1 $atom_2} frame $i]
set d2 [measure bond {$atom_3 $atom_4} frame $i]
puts $outfile "$i , $d1 , $d2"
}
close $outfile
}
The problem here is:
measure bond {$atom_1 $atom_2} frame $i
The issue is that {…} in Tcl actually means “quote this exactly, with no substitutions at all”. Instead of sending a list of two numbers in, it passes a list of two non-numbers (the literal strings $atom_1 and $atom_2).
The fix is to replace {$atom_1 $atom_2} with [list $atom_1 $atom_2].
Yes, proc and for and if make use of this behaviour. It's just that they pass things back to the Tcl interpreter engine as part of their execution.

how do i get words after repeated texts in a line

I have the following line in my file
The image of yours doesnot match the image I had in mind
I need to find the word image in this line and print the next word succeeding it
i.e I need the following o/p:
word_1 = of
word_2 = I
I have regexp command to find the word image but how can I find words succeeding it without having to use lsearch cmd??
You have to use the parameter -inline that returns the array of matching.
So you could have an example here:
set text "The image of yours doesnot match the image I had in mind"
set i 0
set word_1 ""
set word_2 ""
set words [list ]
foreach {img _get} [regexp -all -inline -- {image ([a-zA-Z]+)} $text] {
# print out the word after "image"
puts $_get
# this if you want to save in a list
lappend words $_get
# here you can save on separate variables
if {$i == 0} {
set word_1 $_get
} else {
set word_2 $_get
}
incr i
}
Using a list is a more flexible approach, but if you already know the exact number of words that will match the sentence, than the single variables should fit well.
You can do it like this:
set txt {The image of yours doesnot match the image I had in mind}
set words [split $txt]
for {set i 0} {$i < [llength $words]} {incr i} {
if {[lindex $words $i] eq "image"} {
puts [lindex $words [incr i]]
}
}
This solution looks at each word in sequence. If it is equal to "image" it prints the following word and then continues with the next word in the list.
edit
To save each found word in a variable and use it immediately, replace puts [lindex $words [incr i]] with:
set found [lindex $words [incr i]]
# do something with $found
To save each found word in a list and deal with all the words after finding them all, replace the same line with:
lappend found [lindex $words [incr i]]
It's a good idea to set found to the empty list before searching for words.
Documentation:
< (operator),
eq (operator),
for,
if,
incr,
lappend,
lindex,
llength,
puts,
set,
split

Removing elements from results of glob function in TCL

I am doing :
glob -nocomplain *
as a result I get 4 files:
a b c d
how can I remove from list b?
I am using this func:
proc lremove {args} {
if {[llength $args] < 2} {
puts stderr {Wrong # args: should be "lremove ?-all? list pattern"}
}
set list [lindex $args end-1]
set elements [lindex $args end]
if [string match -all [lindex $args 0]] {
foreach element $elements {
set list [lsearch -all -inline -not -exact $list $element]
}
} else {
# Using lreplace to truncate the list saves having to calculate
# ranges or offsets from the indexed element. The trimming is
# necessary in cases where the first or last element is the
# indexed element.
foreach element $elements {
set idx [lsearch $list $element]
set list [string trim \
"[lreplace $list $idx end] [lreplace $list 0 $idx]"]
}
}
return $list
}
however it does not working with glob results, but only with strings. please help.
That lreplace procedure is rather dodgy, really, what with swapping the order around, ghetto concatenation and string trim to try to clean up the mess. Yuck. Here's a simpler version (without support for -all, which you don't need for processing the output of glob as that's normally a list of unique elements anyway):
proc lremove {list args} {
foreach toRemove $args {
set index [lsearch -exact $list $toRemove]
set list [lreplace $list $index $index]
}
return $list
}
Let's test it!
% lremove {a b c d e} b d f
a c e
Theoretically it could be made more efficient, but it would take a lot of work and be a PITA to debug. This version is way easier to write and is obviously correct. It should also be substantially faster than what you were working with, as it sticks to purely list operations.
The results from glob shouldn't be particularly special that any unusual effort be required to work with them, but there were some really nasty historic bugs that made that not always true. The latest versions of 8.4 and 8.5 (i.e., 8.4.20 and 8.5.15) don't have the bugs. Nor does any release version of 8.6 (8.6.0 or 8.6.1). If stuff is behaving mysteriously, we'll get into asking about versions and telling you to not be quite so behind the times…

Expanded TCL interpreter in TCL

I have implemented many TCL extensions for a specific tool in the domain of formal methods (extensions are implemented in C but I do not want solution to rely on this fact). Thus, the users of my tool can use TCL for prototyping algorithms. Many of them are just linear list of commands (they are powerfull), e.g.:
my_read_file f
my_do_something a b c
my_do_something_else a b c
Now, I am interested in timing. It is possible to change the script to get:
puts [time [my_read_file f] 1]
puts [time [my_do_something a b c] 1]
puts [time [my_do_something_else a b c] 1]
Instead of this I want to define procedure xsource that executes a TCL script and get/write timing for all my commands. Some kind of a profiler. I wrote a naive implementation where the main idea is as follows:
set f [open [lindex $argv 0] r]
set inputLine ""
while {[gets $f line] >= 0} {
set d [expr [string length $line] - 1]
if { $d >= 0 } {
if { [string index $line 0] != "#" } {
if {[string index $line $d] == "\\"} {
set inputLine "$inputLine [string trimright [string range $line 0 [expr $d - 1]]]"
} else {
set inputLine "$inputLine $line"
set inputLine [string trimleft $inputLine]
puts $inputLine
puts [time {eval $inputLine} 1]
}
set inputLine ""
}
}
}
It works for linear list of commands and even allows comments and commands over multiple lines. But it fails if the user uses if statements, loops, and definition of procedures. Can you propose a better approach? It must be pure TCL script with as few extensions as possible.
One way of doing what you're asking for is to use execution traces. Here's a script that can do just that:
package require Tcl 8.5
# The machinery for tracking command execution times; prints the time taken
# upon termination of the command. More info is available too (e.g., did the
# command have an exception) but isn't printed here.
variable timerStack {}
proc timerEnter {cmd op} {
variable timerStack
lappend timerStack [clock microseconds]
}
proc timerLeave {cmd code result op} {
variable timerStack
set now [clock microseconds]
set then [lindex $timerStack end]
set timerStack [lrange $timerStack 0 end-1]
# Remove this length check to print everything out; could be a lot!
# Alternatively, modify the comparison to print more stack frames.
if {[llength $timerStack] < 1} {
puts "[expr {$now-$then}]: $cmd"
}
}
# Add the magic!
trace add execution source enterstep timerEnter
trace add execution source leavestep timerLeave
# And invoke the magic, magically
source [set argv [lassign $argv argv0];set argv0]
# Alternatively, if you don't want argument rewriting, just do:
# source yourScript.tcl
Then you'd call it like this (assuming you've put it in a file called timer.tcl):
tclsh8.5 timer.tcl yourScript.tcl
Be aware that this script has a considerable amount of overhead, as it inhibits many optimization strategies that are normally used. That won't matter too much for uses where you're doing the real meat in your own C code, but when it's lots of loops in Tcl then you'll notice a lot.
You can wrap your commands which you want to measure. And name wrappers exactly as the original ones (renaming original procs before). After that, when instrumented command is executed it actually executes the wrapper, which executes the original procedure and measure the time of execution. The example below (Tcl 8.5).
proc instrument {procs} {
set skip_procs {proc rename instrument puts time subst uplevel return}
foreach p $procs {
if {$p ni $skip_procs} {
uplevel [subst -nocommands {
rename $p __$p
proc $p {args} {
puts "$p: [time {set r [__$p {*}\$args]}]"
return \$r
}
}]
}
}
}
proc my_proc {a} {
set r 1
for {set i 1} {$i <= $a} {incr i} {
set r [expr {$r * $i}]
}
return $r
}
proc my_another_proc {a b} {
set r 0
for {set i $a} {$i <= $b} {incr i} {
incr r $i
}
return $r
}
instrument [info commands my_*]
puts "100 = [my_proc 100]"
puts "200 = [my_proc 100]"
puts "100 - 200 = [my_another_proc 100 200]"
You might want to look at the command "info complete". It can tell you if what you have accumulated so far looks complete from the point of view of most common Tcl syntax markers. It will deal with command input that might be spread across multiple physical lines.