I just started to learn Tcl. I wanted to write a simple procedure.
When the procedure starts, it opens a browse window to browse for files.
There you can select a file you want to open.
Then a pop-up windows comes up and asks if you want to selected another file.
Every file that you select has to go into an array.
I have to following code:
########## Defining the sub procedures ############
proc open_file {} {
set n 0
set title "Select a file"
set types {
{{GDS files} {.gds} }
{{All Files} * }
}
set filename [tk_getOpenFile -filetypes $types -title $title]
set opendFiles($n) $filename
set n [expr $n + 1]
set answer [tk_messageBox -message "Load another GDS file?" -type yesno -icon question]
if {$answer == yes } {
open_file
} else {
show_files ($opendFiles)
}
}
proc show_files {} {
foreach key [array names opendFiles] {
puts $opendFiles($key)
}
}
########## Main Program ###########
open_file
I having the following problems. Because I always recall the proc 'open_file' the variable $n keeps setting to 0. But I don't know how to recall the opening of the window without recalling the whole subroutine....
The second problem is sending the array to the next proc. When I send to the to the proc 'show_files', I always get the next error : can't read "opendFiles": variable is array.
I can't seem to find both answers..
You need global variables for that. This works for me:
########## Defining the sub procedures ############
set n 0
array set openedFiles {}
proc open_file {} {
set title "Select a file"
set types {
{{GDS files} {.gds} }
{{All Files} * }
}
set filename [tk_getOpenFile -filetypes $types -title $title]
set ::openedFiles($::n) $filename
incr ::n
set answer [tk_messageBox -message "Load another GDS file?" -type yesno -icon question]
if {$answer == yes } {
open_file
} else {
show_files
}
}
proc show_files {} {
foreach key [array names ::openedFiles] {
puts $::openedFiles($key)
}
}
########## Main Program ###########
open_file
Array Problem
In Tcl you can't send arrays to procs. You need to convert them to a list with array get send this list to the proc and than convert it back to an array again with array set.
Global variables are very useful at times, but I believe they are best avoided where possible. In this case I'd rather process the loop and the array in the main program rather than the proc.
Also, where you'd use an array in other programming languages, it's often better to use a list in Tcl, so something like:
proc open_file {} {
set title "Select a file"
set types {
{{GDS files} {.gds} }
{{All Files} * }
}
set filename [tk_getOpenFile -filetypes $types -title $title]
return $filename
}
proc show_files {files} {
foreach file $files {
puts $file
}
}
set openedFiles [list]
set answer yes
while {$answer == yes}
lappend openedFiles [open_file]
set answer [tk_messageBox -message "Load another GDS file?" -type yesno -icon question]
}
show_files $openedFiles
If you're into brevity, show_files could be written
proc show_files {files} {
puts [join $files \n]
}
and, now that it's so short, you could just put it in line, rather than have another proc.
Finally, have you considered what you want to do if the user presses cancel in tk_getOpenFile? In this case filename will be set to an empty (zero-length) string. You could either
ignore these; or
get rid of the tk_messageBox call and have the user press cancel when they have entered as many files as they want.
If you want to just ignore those times when the user pressed cancel, you could do
set filename [open_file]
if {[string length $filename] > 0} {
# The user entered a new filesname - add it to the list
lappend openedFiles $filesname
} else {
# The user pressed cancel - just ignore the filename
}
If you wanted to use cancel to break out of the loop, then the main program becomes something like:
set openedFiles [list]
set filename dummy
while {[string length $filename] > 0} {
set filename [open_file]
if {[string length $filename] > 0} {
lappend openedFiles $filename
}
}
show_files $openedFiles
in this case, you might want to put up a message box right at the start of the main program telling the user what's going on.
For the state of a variable to persist between calls to a procedure, you need to make that variable live outside the procedure. The easiest way is to use a global variable:
# Initialize it...
set n 0
proc open_file {} {
# Import it...
global n
...
# Use it...
set openedFiles($n) $filename
incr n
...
}
Arrays are not values, and as such can't be passed directly to another procedure. You can handle this by passing in the name and using upvar 1 to link a local alias to the variable in the calling stack frame:
proc show_files {varName} {
upvar 1 $varName ary
foreach key [array names ary] {
puts $ary($key)
}
}
Which is called using the name of the array, so no $:
show_files openedFiles
(You could also pass a serialization of the array in with array get openedFiles to serialize and array set ary $serialization to deserialize, but that carries some overhead.)
You probably ought to add that openedFiles variable to the global line, so that it is persistent across all invokations of open_file.
Related
I am new to the TCL scripting .I have a script called "Sample.tcl". In the Sample.tcl I have a variable called $name. How can I assign a value to the variable if there exist a specific argv i.e.
Sample.tcl -step xyz
Only if I specify -step then $name should be xyz.
I'm not sure what $name might be in this context (it's a really unusual name for a variable, and using variable variable names is typically a bad idea) but under the guess that you're trying to set step to xyz in this case, you can put this in your script:
apply {{} {
# For each pair of values in the arguments (after the script name)
global argv
foreach {key value} $argv {
# Safety-check: if the key starts with a hyphen...
if {[string match -* $key]} {
# ... strip the leading hyphen(s)
set varname [string trimleft $key "-"]
# ... bind that global var name to a local name
upvar 1 $varname var
# ... and set the variable to the value we've got.
set var $value
}
}
}}
It's done in an apply so that we don't pollute the global namespace with all our working variables (key, value, varname and var) and because we don't really need to make a procedure for something we're only going to do once.
This isn't a safe piece of code, not by any means, but it is a useful and flexible way to get something working.
In general, parsing command line arguments can take quite a bit of thought to get perfectly right and there's various packages to help out, but that's only really important when writing code for other people to run. When it's just for yourself, you can be a lot sloppier and get the job done in a few minutes.
Using the cmdline package from tcllib you could write:
#!/usr/bin/env tclsh
package require cmdline
set options {
{step.arg "" "Set the step value"}
}
try {
array set params [cmdline::getoptions argv $options]
} on error e {
puts stderr $e
exit 1
}
if {$params(step) ne ""} {
set name $params(step)
}
if {[info exists name]} {
puts "name = $name"
} else {
puts "name is not set"
}
I have a variable, let's say xx, with a list of index 0 and index 1 values. I want to modify a script (not mine) which previously defines a function, pptable, i.e.,
proc pptable {l1 l2} {
foreach i1 $l1 i2 $l2 {
puts " [format %6.2f $i1]\t[format %6.2f $i2]"
}
}
so that it displays the output into two columns using
pptable [lindex $xx 1] [lindex $xx 0]
However, I want to write the output directly to a file. Could you tell me how I can send the data to a file instead to the display?
One of the neatest ways of doing this is to stack on a channel transform that redirects stdout to where you want it to go. This works even if the write to stdout happens from C code or in a different thread as it plugs into the channel machinery. The code is a little bit long (and requires Tcl 8.6) but is reliable and actually mostly very simple.
package require Tcl 8.6; # *REQUIRED* for [chan push] and [chan pop]
proc RedirectorCallback {targetHandle op args} {
# The switch/lassign pattern is simplest way of doing this in one procedure
switch $op {
initialize {
lassign $args handle mode
# Sanity check
if {$mode ne "write"} {
close $targetHandle
error "this is just a write transform"
}
# List of supported subcommands
return {initialize finalize write}
}
finalize {
lassign $args handle
# All we need to do here is close the target file handle
close $targetHandle
}
write {
lassign $args handle buffer
# Write the data to *real* destination; this does the redirect
puts -nonewline $targetHandle $buffer
# Stop the data going to *true* stdout by returning empty string
return ""
# If we returned the data instead, this would do a 'tee'
}
default {
error "unsupported subcommand"
}
}
}
# Here's a wrapper to make the transform easy to use
proc redirectStdout {file script} {
# Stack the transform onto stdout with the file handle to write to
# (which is going to be $targetHandle in [redirector])
chan push stdout [list RedirectorCallback [open $file "wb"]]
# Run the script and *definitely* pop the transform after it finishes
try {
uplevel 1 $script
} finally {
chan pop stdout
}
}
How would we actually use this? It's really very easy in practice:
# Exactly the code you started with
proc pptable {l1 l2} {
foreach i1 $l1 i2 $l2 {
puts " [format %6.2f $i1]\t[format %6.2f $i2]"
}
}
# Demonstrate that stdout is working as normal
puts "before"
# Our wrapped call that we're capturing the output from; pick your own filename!
redirectStdout "foo.txt" {
pptable {1.2 1.3 1.4} {6.9 6.8 6.7}
}
# Demonstrate that stdout is working as normal again
puts "after"
When I run that code, I get this:
bash$ tclsh8.6 stdout-redirect-example.tcl
before
after
bash$ cat foo.txt
1.20 6.90
1.30 6.80
1.40 6.70
I believe that's precisely what you are looking for.
You can do this with less code if you use Tcllib and TclOO to help deal with the machinery:
package require Tcl 8.6
package require tcl::transform::core
oo::class create WriteRedirector {
superclass tcl::transform::core
variable targetHandle
constructor {targetFile} {
set targetHandle [open $targetFile "wb"]
}
destructor {
close $targetHandle
}
method write {handle buffer} {
puts -nonewline $targetHandle $buffer
return ""
}
# This is the wrapper, as a class method
self method redirectDuring {channel targetFile script} {
chan push $channel [my new $targetFile]
try {
uplevel 1 $script
} finally {
chan pop $channel
}
}
}
Usage example:
proc pptable {l1 l2} {
foreach i1 $l1 i2 $l2 {
puts " [format %6.2f $i1]\t[format %6.2f $i2]"
}
}
puts "before"
WriteRedirector redirectDuring stdout "foo.txt" {
pptable {1.2 1.3 1.4 1.5} {6.9 6.8 6.7 6.6}
}
puts "after"
I assume you don't want or can't modify the existing script and proc pptable, correct?
If so, there are different options, depending on your exact situation:
Redirect stdout: tclsh yourscript.tcl > your.out
Redefine puts (for a clearly defined scope):
rename ::puts ::puts.orig
proc puts args {
set fh [open your.out w];
::puts.orig $fh $args;
close $fh
}
# run pptable, source the script
This theme has been covered before, e.g., tcl stop all output going to stdout channel?
Rewire Tcl's stdout channel (not necessarily recommended):
close stdout
open your.out w
# run pptable, source the script
This has also been elaborated on before, e.g. Tracing stdout and stderr in Tcl
I have a file like this:
set position {0.50 0.50}
set visibility false
set text {ID: {entity.id}\n Value: {entity.contour_val}}
And I want to do something similar to source, but I want to use a file handle only.
My current attempt looks like this:
proc readArray {fileHandle arrayName} {
upvar $arrayName arr
set cl 0
while {! [eof $fileHandle]} {
set cl [expr "$cl + 1"]
set line [gets $fileHandle]
if [$line eq {}] continue
puts $line
namespace eval ::__esg_priv "
uplevel 1 {*}$line
"
info vars ::__esg_priv::*
foreach varPath [info vars ::__esg_priv::*] {
set varName [string map { ::__esg_priv:: "" } $varPath]
puts "Setting arr($varName) -> [set $varPath]"
set arr($varName) [set $varPath]
}
namespace delete __esg_priv
}
puts "$cl number of lines read"
}
In place of uplevel I tried many combinations of eval and quoting.
My problem is, it either fails on the lines with lists or it does not actuall set the variables.
What is the right way to do it, if the executed commands are expected to be any valid code.
An extra question would be how to properly apply error checking, which I haven't tried yet.
After a call to
readArray [open "myFile.tcl" r] arr
I expect that
parray arr
issues something like:
arr(position) = 0.50 0.50
arr(text) = ID: {entity.id}\n Value: {entity.contour_val}
arr(visibility) = false
BTW: The last line contains internal {}, which are supposed to make it into the string variables. And there is no intent to make this a dict.
This code works, but there are still some problems with it:
proc readArray {fileHandle arrayName} {
upvar $arrayName arr
set cl 0
while {! [eof $fileHandle]} {
incr cl ;# !
set line [gets $fileHandle]
if {$line eq {}} continue ;# !
puts $line
namespace eval ::__esg_priv $line ;# !
foreach varPath [info vars ::__esg_priv::*] {
set varName [string map { ::__esg_priv:: "" } $varPath]
puts "Setting arr($varName) -> [set $varPath]"
set arr($varName) [set $varPath]
}
namespace delete __esg_priv
}
puts "$cl number of lines read"
}
I've taken out a couple of lines that didn't seem necessary, and changed some lines a bit.
You don't need set cl [expr "$cl + 1"]: incr cl will do.
if [$line eq {}] continue will fail because the [...] is a command substitution. if {$line eq {}} continue (braces instead of brackets) does what you intend.
Unless you are accessing variables in another scope, you won't need uplevel. namespace eval ::__esg_priv $line will evaluate one line in the designated namespace.
I didn't change the following, but maybe you should:
set varName [string map { ::__esg_priv:: "" } $varPath] works as intended, but set varName [namespace tail $varPath] is cleaner.
Be aware that if there exists a global variable with the same name as one of the variables in your file, no namespace variable will be created; the global variable will be updated instead.
If you intend to use the value in the text variable as a dictionary, you need to remove either the \n or the braces.
According to your question title, you want to evaluate the file line by line. If that requirement can be lifted, your code could be simplified by reading the whole script in one operation and then evaluating it with a single namespace eval.
ETA
This solution is a lot more robust in that it reads the script in a sandbox (always a good idea when writing code that will execute arbitrary external code) and redefines (within that sandbox) the set command to create members in your array instead of regular variables.
proc readArray {fileHandle arrayName} {
upvar 1 $arrayName arr
set int [interp create -safe]
$int alias set apply {{name value} {
uplevel 1 [list set arr($name) $value]
}}
$int eval [read $fileHandle]
interp delete $int
}
To make it even more safe against unexpected interaction with global variables etc, look at the interp package in the Tcllib. It lets you create an interpreter that is completely empty.
Documentation: apply, continue, eof, foreach, gets, if, incr, info, interp package, interp, list, namespace, proc, puts, set, string, uplevel, upvar, while
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;
}
}
I'm producing a TCL procedure that will return a list of arrays of devices under a switch. The definition is an XML file that is read. The resulting lists of XML entries are parsed using a recursive procedure and the device attributes are placed in an array.
Each array is then placed in a list and reflected back to the caller. My problem is that when I print out the list of devices, the last device added to the list is printed out each time. The contents of the list is all duplicates.
Note: I'm using the excellent proc, 'xml2list' that was found here. I'm sorry, I forgot who submitted this.
The following code illustrates the problem:
source C:/src/tcl/xml2list.tcl
# Read and parse XML file
set fh [open C:/data/tcl/testfile.xml r]
set myxml [read $fh]
set mylist [xml2list $myxml]
array set mydevice {}
proc devicesByName { name thelist list_to_fill} {
global mydevice
global set found_sw 0
upvar $list_to_fill device_arr
foreach switch [lindex $thelist 2] {
set atts [lindex $switch 1]
if { [lindex $switch 0] == "Switch" } {
if { $name == [lindex $atts 3] } {
set found_sw 1
puts "==== Found Switch: $name ===="
} else {
set found_sw 0
}
} elseif { $found_sw == 1 && [string length [lindex $atts 3]] > 0 } {
set mydevice(hdr) [lindex $switch 0]
set mydevice(port) [lindex $atts 1]
set mydevice(name) [lindex $atts 3]
set mydevice(type) [lindex $atts 5]
puts "Device Found: $mydevice(name)"
set text [lindex $switch 2]
set mydevice(ip) [lindex [lindex $text 0] 1]
lappend device_arr mydevice
}
devicesByName $name $switch device_arr
}
}
#--- Call proc here
# set a local array var and send to the proc
set device_arr {}
devicesByName "Switch1" $mylist device_arr
# read out the contents of the list of arrays
for {set i 0} {$i<[llength $device_arr]} {incr i} {
upvar #0 [lindex $device_arr $i] temp
if {[array exists temp]} {
puts "\[$i\] Device: $temp(name)-$temp(ip)"
}
}
The XML file is here:
<Topology>
<Switch ports="48" name="Switch1" ip="10.1.1.3">
<Device port="1" name="RHEL53-Complete1" type="host">10.1.1.10</Device>
<Device port="2" name="Windows-Complete1" type="host">10.1.2.11</Device>
<Device port="3" name="Solaris-Complete1" type="host">10.1.2.12</Device>
</Switch>
<Switch ports="36" name="Switch2" ip="10.1.1.4">
<Device port="1" name="Windows-Complete2" type="host">10.1.3.10</Device>
</Switch>
<Router ports="24" name="Router1" ip="10.1.1.2">
<Device port="1" name="Switch1" type="switch">10.1.1.3</Device>
<Device port="2" name="Switch2" type="switch">10.1.1.4</Device>
</Router>
</Topology>
If my code blocks look bad, please excuse that. I followed the directions as I read them, but it didn't look correct. I could not fix it, so just posted anyway.
Thanks in advance...
Arrays in tcl are not values. Therefore they don't behave like regular variables. They are in fact something special like filehandles or sockets.
You cannot assign an array to a list like that. Doing:
lappend device_arr mydevice
simply appends the string "mydevice" to the list device_arr. That string happens to be the name of a global variable so that string may be used later to access that global variable.
To build up a key-value data structure what you want is a dict. You can think of a dict as a special list that has even numbers of elements in the format: {key value key value}. In fact, this data structure works even on very old versions of tcl before the introduction of dicts because the foreach loop in tcl can be used to process key-value pairs.
So what you want is to create a new $mydevice dict each loop and use [dict set] to assign the values.
Alternatively you can keep most of your code and change your lappend to:
lappend device_arr [array get mydevice]
This works because [array get] returns a key-value list which can be treated as a dict. You can later access the data using the dict command.
Array variables can't be used as values. To put the contents of one into a list element, send it to a proc, write it to a file etc, convert it to list form (key, value, key, value...) with array get.
lappend device_arr [array get mydevice]
To use it later, write the list back to an array with array set.
foreach device_l $device_arr {
#array unset device
array set device $device_l
puts "$device(name)-$device(ip)"
}
Note that array set doesn't erase the old keys in the destination array, so if you use it in a loop and the key names aren't always the same, you need to clear the array every iteration.
You can store this information in two ways using arrays . First is as a multi-dimensional array, in this case a three dimensional array and the second is a one dimensional array storing a list that can be converted easily to an array later for accessing data at a later time.
For the 3d array the key would be Switch Name,device_port,dataname you would change your erroneous temporary myDevice and lappend code to
# attr is a list of { attributename1 value1 ... attributenameN valueN}
array set temp $attr
set port $temp(port)
set text [lindex $switch 2]
set ip [lindex [lindex $text 0] 1]
# name already set to "Switch1" etc
foreach f [array names temp ] {
set device_arr($name,$port,$f) $temp($f)
}
set device_arr($name,$port,ip) $ip
array unset temp
this code results in the following ( when parray device_arr
parray device_arr
device_arr(Switch1,1,name) "Switch1"
device_arr(Switch1,1,port) 1
device_arr(Switch1,1,type) "RedHat .."
device_arr(Switch1,1,ip) 10..
device_arr(Switch1,2,name) "Switch1"
device_arr(Switch1,2,port) 1
device_arr(Switch1,2,type) "RedHat .."
device_arr(Switch1,2,ip) 10..
...
device_arr(Switch2,1,name) "Switch1"
device_arr(Switch2,1,port) 1
device_arr(Switch2,1,type) "Windows Complete"
device_arr(Switch2,1,ip) 10..
....
to find ip of Switch1 port2 you would:
puts "the ip of Switch1 port 2 is $device_arr(Switch1,2,ip)"
Note lots of data duplication but you can access all data directly without having to go to an intermediate step to get to the data as in the next scheme
# attr is a list of { attributename1 value1 ... attributenameN valueN}
set data $attr
array set temp $attr
set text [lindex $switch 2]
set ip [lindex [lindex $text 0] 1]
lappend data ip $ip
set key "$name,$temp(port)"
# name already set to "Switch1" etc
set device_arr($name,$port) $data
array unset temp
doing a parray device_arr gives:
device_arr(Switch1,1) { port "1" name "RHEL53-Complete1" type "host" ip 10.1.1.10 }
device_arr(Switch1,2) { port "2" name "Windows-Complete1" type "host" ip 10.1.2.11}
....
to find the ip of swtich1 port 2 you would
array set temp $device_array(Switch1,2)
puts "ip of device 2 is $temp(ip)"