tcl open pipe seems to misshandle spaces in parameters - tcl

I have this open:
set r [catch {open "|[concat $config(cmd,sh) [list $cmd 2>#1]]" r} fid]
where $config(cmd,sh) is cmd /c and I am trying to pass a file name (and possibly a command such as echo) in $cmd. If there is no space in the file name, i.e. :
cmd is echo /filename
all is well. With a space, i.e.:
cmd is echo "/file name"
what appears to be passed is:
\"file name\".
When I try this on Linux, I get "file name" (no backslashes). I have tried replacing the spaces in the file name with "\ ", but then the target gets two file names, i.e. the space is used to break up the file name.
I am beginning to think I have found a bug in the Windows port of Tcl...

Ugh, that looks convoluted! To pass this sort of thing into the pipe creation code, you need to use exactly the right recipe:
set r [catch {open |[list {*}$config(cmd,sh) $cmd 2>#1] r} fid]
That is, always use the form with |[list ...] when building pipes as the documentation says that is what the pipe opener looks for. (This is the only command like that in Tcl.)
And of course, using the (8.5+) {*} syntax is much simpler in this case too, as it is more obviously doing the right thing.

Related

Reading cmd arguments in TCL file

I am trying to run a tcl script through .bat file. I want to read some cmd arguments in the tcl script. Below is my code:
Command to run:
D:\Cadence\Sigrity2021.1\tools\bin\PowerSI.exe -tcl abcd.tcl %new_var%.spd %new_file_name%
Below is how I am trying to read the variable in the tcl file:
sigrity::open document [lindex $argv 0] {!}
It open up the Cadence Sigrity, but I see the below error:
How do I read cmd argument in tcl?
If you have no other way to do it that you can find (and it sounds like that might be the case) then you can fake it by writing a helper file with content like this, filling in the real arguments in the appropriate places:
# Name of script to call
set ::argv0 "abcd.tcl"
# Arguments to pass
set ::argv {}
lappend ::argv "%new_var%.spd"
lappend ::argv "%new_file_name%"
# Number of arguments (rarely used)
set ::argc [llength $::argv]
# Do the call
source $::argv0
Then you can pass that file to PowerSI and it will set things up and chain to the real file. It's messy, but practical.
If you're writing this from Tcl, use the list command to do the quoting of the strings (instead of putting them in double quotes) as it will do exactly the right thing for you. If you're writing the file from another language, you'll want to make sure you put backslashes in before \, ", $ and [ characters. The fiddlyness of doing that depends on your language.

In Tcl, how do I prevent a variable's contents from being interpreted by the shell when passed to `exec`?

I'm maintaining some old code and found that the following piece...
if {[catch {exec -- echo $html_email > $file} ret]} {
puts $ret
return 0
}
...breaks due to the first character of an HTML email being <, i.e.
couldn't read file "html>
<title>cama_Investigate 00000560554PONY1</title>
<style type="text/css">
...
...
...
which is interpreted as an I/O redirect operator. Previously this wasn't an issue because we were starting the emails with some headers, e.g.
append html_email "Content-Type : text/html; charset=us-ascii\n"
append html_email "Content-Disposition: inline\n"
I'm going to rewrite all this to use Tcl's native file I/O, so this question is mainly academic: What is the proper way to guard a variable's contents from being interpreted by the shell when passed to exec?
I'm using Tcl 8.0.5 and csh, but I'm interested in a general answer if possible.
Tcl's exec is funky, alas. It insists on interpreting an argument that starts with a < character as a redirect. (There are a few other ones too, but you're a bit less likely to hit them.) There isn't a good general workaround either except to write the data to a temporary file and redirect from that.
set ctr 0
while 1 {
set filename /tmp/[pid].[incr ctr].txt
# POSIX-style flags; write-only, must create or generate error
if {[catch {open $filename {WRONLY CREAT EXCL}} f] == 0} break
}
puts $f $html_email
close $f
exec echo <$filename >$file
file delete $filename
This is horribly complicated! We can do much better by changing what program we use. If instead of using echo we use cat, we can use exec's heredoc syntax:
exec cat <<$html_email >$file
Since in this case the characters are being passed directly via a pipeline (which is how Tcl does this) there's far less to go wrong. Yet it's still silly since Tcl's entirely capable of writing to files directly, more portably, and with less overhead:
set f [open $file "w"]
puts $f $html_email
close $f
Yes, this is actually a hugely simplified version of the general replacement from the first example above. Let's do the simple things that are much more obviously correct since then there's less to surprise in the future.
You can invoke the intended command indirectly, routing it through a shell:
exec -- csh -c "echo '$html_email'" > $file
or
exec -- csh -c "exec echo '$html_email'" > $file

How to zip multiple files through tcl script in Linux box?

I have set of code in tcl where I'm trying to achieve to zip the files but I'm getting below error
zip warning: name not matched: a_1.txt a_2.txt a_3.txt a_4.txt
On other hand I'm doing same thing from command prompt I'm able to execute successfully.
#!/usr/local/bin/tclsh
set outdir /usr/test/
set out_files abc.10X
array set g_config { ZIP /usr/bin/zip }
set files "a_1.txt a_2.txt a_3.txt a_4.txt"
foreach inp_file $files {
append zipfiles "$inp_file "
}
exec $g_config(ZIP) $outdir$out_files zipfiles
Tcl really cares about the boundaries between words, and doesn't split things up unless asked to. This is good as it means that things like filenames with spaces in don't confuse it, but in this case it causes you some problems.
To ask it to split the list up, precede the read of the word from the variable with {*}:
exec $g_config(ZIP) $outdir$out_files {*}$files
This is instead of this:
exec $g_config(ZIP) $outdir$out_files $files
# Won't work; uses "strange" filename
or this:
exec $g_config(ZIP) $outdir$out_files zipfiles
# Won't work; uses filename that is the literal "zipfiles"
# You have to use $ when you want to read from a variable and pass the value to a command.
Got a very old version of Tcl where {*} doesn't work? Upgrade to 8.5 or 8.6! Or at least use this:
eval {exec $g_config(ZIP) $outdir$out_files} $files
(You need the braces there in case you put a space in outdir…)

taking file content as file name itself in tcl

I am running one tcl script who is taking file as a input by "stdin".The problem is that its taking the file content as a filename and throwing error while running the script on command line processor.
tcl script is
#!/bin/sh
# SystemInfo_2.tcl \
exec tclsh "$0" ${1+"$#"}
set traps [read stdin];
#set traps "snmp trap test"
set timetrap [clock format [clock seconds]];
set trapout [open Database_traps_event.txt a+];
set javaout [open JavaTrapOutput.txt a+];
puts $trapout $timetrap;
puts $trapout $traps;
puts $trapout "Before executing java program";
set javaprogargs "open {|java -cp mysql-connector-java-5.1.10.jar;. EventAlarmHandling \"$traps\"} r";
puts $trapout $javaprogargs;
set javaprogram [eval $javaprogargs];
puts $trapout "Execution of java is over"
while { [gets $javaprogram line] != -1 } {
puts $javaout $line;
}
close $javaprogram;
puts $trapout "After excution of java program\r\n\r\n\r\n\r\n\r\n";
close $trapout;
close $javaout;
exit;
input file content is -
<UNKNOWN>
UDP: [192.168.1.19]:60572->[0.0.0.0]:0
.iso.org.dod.internet.mgmt.mib-2.system.sysUpTime.sysUpTimeInstance 1:9:58:56.61
.iso.org.dod.internet.snmpV2.snmpModules.snmpMIB.snmpMIBObjects.snmpTrap.snmpTrapOID.0 .iso.org.dod.internet.snmpV2.snmpModules.snmpMIB.snmpMIBObjects.snmpTraps.linkDown
.iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifIndex.1 8
.iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifAdminStatus.8 up
.iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifOperStatus.8 down
From command line it ran like below
E:\eventAlarmHandling>tclsh TclTempFile.tcl < traps.txt
couldn't read file "UNKNOWN>
UDP: [192.168.1.19]:60572->[0.0.0.0]:0
.iso.org.dod.internet.mgmt.mib-2.system.sysUpTime.sysUpTimeInstance 1:9:58:56.61
.iso.org.dod.internet.snmpV2.snmpModules.snmpMIB.snmpMIBObjects.snmpTrap.snmpTrapOID.0 .iso.org.dod.intern
et.snmpV2.snmpModules.snmpMIB.snmpMIBObjects.snmpTraps.linkDown
.iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifIndex.1 8
.iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifAdminStatus.8 up
.iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifOperStatus.8 down": No error
while executing
"open {|java -cp mysql-connector-java-5.1.10.jar;. EventAlarmHandling "<UNKNOWN>
UDP: [192.168.1.19]:60572->[0.0.0.0]:0
.iso.org.dod.internet.mgmt.mib..."
("eval" body line 1)
invoked from within
"eval $javaprogargs"
invoked from within
"set javaprogram [eval $javaprogargs]"
(file "TclTempFile.tcl" line 26)
So clearly in command line its showing that "couldn't read file UNKNOWN> ......"
So please explain it that whats happening here in command line.I am new to tcl.So hoping that someone help me out.
Thanks
You're having problems with one of the trickier bits of how pipelines work in Tcl. If we look at the documentation carefully, we see:
If the first character of fileName is “|” then the remaining characters of fileName are treated as a list of arguments that describe a command pipeline to invoke, in the same style as the arguments for exec.
That means you have to have the first character be | and the rest, after stripping that first character, be a proper list. In your case, you've not got that. Instead, you're doing:
set javaprogargs "open {|java -cp mysql-connector-java-5.1.10.jar;. EventAlarmHandling \"$traps\"} r";
That's pretty complicated anyway. Let's build this in the idiomatic fashion instead:
set CPsep ";"
set classpath [list mysql-connector-java-5.1.10.jar .]
set javaprogargs [list open |[list \
java -cp [join $classpath $CPsep] EventAlarmHandling $traps]]
It helps to split the classpath out; it's got a ; character in it (on Windows; you'll need to change that if you port to Linux or OSX) and it's nicer to use list in Tcl to build things and then join to convert into what Java expects.
We also no longer need any backslash-quoted substrings in there (except the one I put in to keep lines short and readable); the pattern of list commands there will add everything that is required. Note the |[list …] there: that's non-idiomatic everywhere in Tcl except when creating a pipeline when it is recommended practice as it is doing in reverse what open expects to parse.
The other thing you're running into is this:
If an arg (or pair of args) has one of the forms described below then it is used by exec to control the flow of input and output among the subprocess(es). Such arguments will not be passed to the subprocess(es).
[…]
< fileName
The file named by fileName is opened and used as the standard input for the first command in the pipeline.
Your argument from $traps starts with a < and so it triggers this rule.
Unfortunately, there's no simple workaround for this and this is a severe, known, and very annoying limitation of the pipeline creation code. The only known techniques for dealing with this are to move to transferring that data by either a file or via the subprocess's standard input, both of which require modifying the subprocess's implementation. If you can make that Java program read from System.in (a good idea anyway, so you don't hit Windows's command line length limitations!) then you can pass the value like this:
set CPsep ";"
set classpath [list mysql-connector-java-5.1.10.jar .]
set javaprogargs [list open |[list \
java -cp [join $classpath $CPsep] EventAlarmHandling << $traps]]
That is just by adding a << in there immediately before the value.

TCL- script to output a file which contains size of all the files in the directry and subdirectories

Please help me with the script which outputs the file that contains names of the files in subdirectories and its memory in bytes, the arguement to the program is the folder path .output file should be file name in 1st column and its memory in second column
Note:folder contains subfolders...inside subfolders there are files
.I tried this way
set fp [open files_memory.txt w]
set file_names [glob ../design_data/*/*]
foreach file $file_names {
puts $fp "$file [lindex [exec du -sh $file] 0]"
}
close $fp
Result sample:
../design_data/def/ip2.def.gz 170M
../design_data/lef/tsmc13_10_5d.lef 7.1M
But i want only file name to be printed that is ip2.def.gz , tsmc13_10_5d.lef ..etc(not the entirepath) and file memorry should be aligned
TCL
The fileutil package in Tcllib defines the command fileutil::find, which can recursively list the contents of a directory. You can then use foreach to iterate over the list and get the sizes of each of them with file size, before producing the output with puts, perhaps like this:
puts "$filename\t$size"
The $filename is the name of the file, and the $size is how large it is. You will have obtained these values earlier (i.e., in the line or two before!). The \t in the middle is turned into a TAB character. Replace with spaces or a comma or virtually anything else you like; your call.
To get just the last part of the filename, I'd do:
puts $fp "[file tail $file] [file size $file]"
This does stuff with the full information about the file size, not the abbreviated form, so if you really want 4k instead of 4096, keep using that (slow) incantation with exec du. (If the consumer is a program, or a programmer, writing out the size in full is probably better.)
In addition to Donal's suggestion, there are more tools for getting files recursively:
recursive_glob (from the Tclx package) and
for_recursive_glob (also from Tclx)
fileutil::findByPattern (from the fileutil package)
Here is an example of how to use for_recursive_glob:
package require Tclx
for_recursive_glob filename {../design_data} {*} {
puts $filename
}
This suggestion, in combination with Donal's should be enough for you to create a complete solution. Good luck.
Discussion
The for_recursive_glob command takes 4 arguments:
The name of the variable representing the complete path name
A list of directory to search for (e.g. {/dir1 /dir2 /dir3})
A list of patterns to search for (e.g. {*.txt *.c *.cpp})
Finally, the body of the for loop, where you want to do something with the filename.
Based on my experience, for_recursive_glob cannot handle directories that you don't have permission to (i.e. on Mac, Linux, and BSD platforms, I don't know about Windows). In which case, the script will crash unless you catch the exception.
The recursive_glob command is similar, but it returns a list of filenames instead of structuring in a for loop.