How to variable substitution when reading a file in Tcl - tcl

Here is my file name text and it has below contents
system
{
host name $HOSTNAME
value 25
as 635
}
I am writing below code to read the file :
set HOSTNAME "NEw york"
set cf [open text r]
foreach line [split [read $cf] \n] {
puts $line
close $cf
When I do puts $line variable substitution for HOSTNAME is not done, how to do that ??

Use the subst command:
https://www.tcl.tk/man/tcl8.4/TclCmd/subst.html
set HOSTNAME "New York"
set line {host name $HOSTNAME}
puts $line
--> "host name $HOSTNAME"
puts [subst $line]
--> "host name New York"

If you only want to support substituting $HOSTNAME and nothing else, use string map:
set HOSTNAME "NEw york"
set mapping [list "\$HOSTNAME" $HOSTNAME]
set cf [open text r]
foreach line [split [read $cf] \n] {
puts [string map $hostname $line]
}
close $cf
The string map command is unreasonably great for doing any fixed set of replacements. While I know its implementation is internally very stupid, it has superb cache coherency and is exactly the sort of thing that CPU makers like to optimize for.

Related

TCL: Read lines from file that contain only relevant words

I'm reading file and make some manipulation on the data.
Unfortunately I get the below error message:
unable to alloc 347392 bytes
Abort
Since the file is huge, I want to read only the lines that contain some word (describe in "regexp_or ")
Is there any way to read only the lines that contain "regexp_or" and save the foreach loop?
set regexp_or "^Err|warning|Fatal error"
set file [open [lindex $argv 1] r]
set data [ read $file ]
foreach line [ split $data "\n" ] {
if {[regexp [subst $regexp_or] $line]} {
puts $line
}
}
You could pull your input through grep:
set file [open |[list grep -E $regexp_or [lindex $argv 1]] r]
But that depends on grep being available. To do it completely in Tcl, you can process the file in chunks:
set file [open [lindex $argv 1] r]
while {![eof $file]} {
# Read a million characters
set data [read $file 1000000]
# Make sure to only work with complete lines
append data [gets $file]
foreach line [lsearch -inline -all -regexp [split $data \n] $regexp_or] {
puts $line
}
}
close $file

SaveTCL input Entry in a file and later load it

I have developed a TCL UI with couple of inputs needs to be entered by user. First time user will enter all files path but then i wanted to save the user defined entries in a file and then later load it.
Saving is fine... i think of saving all these variables in a file, but loading it from a file needs a mapping, how it can be done ?
Any example will be helpful
If you have the flexibility to define the format of the file where the contents will be stored, I'd recommend storing the contents in a way such that reading/writing maps to the keys and is order independent. This will allow you to update your UI to add/delete input fields without worrying about the order in which they are captured in the file.
For e.g., your file format could be something this:
Top Directory: <value>
LEF File: <value>
.
.
.
You'll have to carefully choose a separator between the key (label) and the value.
If this is going to be used in TCL always, you can make it simpler by storing an array in a file. This'll also speed up when you load the file to populate the entries in the UI. For e.g., your file format could be something this:
set inputFields("Top Directory") <value>
set inputFields("LEF File") <value>
I achieved this by following code, though not very optimized.
First i am saving a input file with variable values and then reading them in same order.
proc save_input_entries {} {
global ENTRYfilename ENTRYfilename2 ENTRYfilename3 ENTRYfilename4 ENTRYfilename5 ENTRYfilename6 ENTRYfilename7 ENTRYfilename8 ENTRYfilename9 ENTRYfilename10 ENTRYfilename11 ENTRYfilename12 ENTRYfilename13 ENTRYfilename14 ENTRYfilename15 ENTRYfilename16 ENTRYfilename17 topdir corner_dir corner_name
set filename Input_entries.txt
set fileId [open $filename "w"]
puts $fileId $ENTRYfilename
puts $fileId $ENTRYfilename3
puts $fileId $ENTRYfilename4
puts $fileId $ENTRYfilename5
puts $fileId $ENTRYfilename7
puts $fileId $ENTRYfilename8
puts $fileId $ENTRYfilename15
puts $fileId $ENTRYfilename14
puts $fileId $ENTRYfilename16
puts $fileId $ENTRYfilename17
close $fileId
}
proc load_input_entries {} {
global ENTRYfilename ENTRYfilename2 ENTRYfilename3 ENTRYfilename4 ENTRYfilename5 ENTRYfilename6 ENTRYfilename7 ENTRYfilename8 ENTRYfilename9 ENTRYfilename10 ENTRYfilename11 ENTRYfilename12 ENTRYfilename13 ENTRYfilename14 ENTRYfilename15 ENTRYfilename16 ENTRYfilename17
set fp [open Input_entries.txt]
set stuff [read $fp]
set lines [split $stuff "\n"]
set ENTRYfilename [lindex $lines 0]
set ENTRYfilename3 [lindex $lines 1]
set ENTRYfilename4 [lindex $lines 2]
set ENTRYfilename5 [lindex $lines 3]
set ENTRYfilename7 [lindex $lines 4]
set ENTRYfilename8 [lindex $lines 5]
set ENTRYfilename15 [lindex $lines 6]
set ENTRYfilename14 [lindex $lines 7]
set ENTRYfilename16 [lindex $lines 8]
set ENTRYfilename17 [lindex $lines 9]
}

TCL: Check file existance by SHELL environment variable (another one)

I have a file contain lines with path to the files. Sometimes a path contain SHELL environment variable and I want to check the file existence.
The following is my solution:
set fh [open "the_file_contain_path" "r"]
while {![eof $fh]} {
set line [gets $fh]
if {[regexp -- {\$\S+} $line]} {
catch {exec /usr/local/bin/tcsh -c "echo $line" } line
if {![file exists $line]} {
puts "ERROR: the file $line is not exists"
}
}
}
I sure there is more elegant solution without using
/usr/local/bin/tcsh -c
You can capture the variable name in the regexp command and do a lookup in Tcl's global env array. Also, your use of eof as the while condition means your loop will interate one time too many (see http://phaseit.net/claird/comp.lang.tcl/fmm.html#eof)
set fh [open "the_file_contain_path" "r"]
while {[gets $fh line] != -1} {
# this can handle "$FOO/bar/$BAZ"
if {[string first {$} $line] != -1} {
regsub -all {(\$)(\w+)} $line {\1::env(\2)} new
set line [subst -nocommand -nobackslashes $new]
}
if {![file exists $line]} {
puts "ERROR: the file $line does not exist"
}
}
First off, it's usually easier (for small files, say of no more than 1–2MB) to read in the whole file and split it into lines instead of using gets and eof in a while loop. (The split command is very fast.)
Secondly, to do the replacement you need the place in the string to replace, so you use regexp -indices. That does mean that you need to take a little more complex approach to doing the replacement, with string range and string replace to do some of the work. Assuming you're using Tcl 8.5…
set fh [open "the_file_contain_path" "r"]
foreach line [split [read $fh] "\n"] {
# Find a replacement while there are any to do
while {[regexp -indices {\$(\w+)} $line matchRange nameRange]} {
# Get what to replace with (without any errors, just like tcsh)
set replacement {}
catch {set replacement $::env([string range $line {*}$nameRange])}
# Do the replacement
set line [string replace $line {*}$matchRange $replacement]
}
# Your test on the result
if {![file exists $line]} {
puts "ERROR: the file $line is not exists"
}
}
TCL programs can read environment variables using the built-in global variable env. Read the line, look for $ followed by a name, look up $::env($name), and substitute it for the variable.
Using the shell for this is very bad if the file is supplied by untrusted users. What if they put ; rm * in the file? And if you're going to use a shell, you should at least use sh or bash, not tcsh.

TCL - find a regular pattern in a file and return the occurrence and number of occurrences

I am writing a code to grep a regular expression pattern from a file, and output that regular expression and the number of times it has occured.
Here is the code: I am trying to find the pattern "grep" in my file hello.txt:
set file1 [open "hello.txt" r]
set file2 [read $file1]
regexp {grep} $file2 matched
puts $matched
while {[eof $file2] != 1} {
set number 0
if {[regexp {grep} $file2 matched] >= 0} {
incr number
}
puts $number
}
Output that I got:
grep
--------
can not find channel named "qwerty
iiiiiii
wxseddtt
lsakdfhaiowehf'
jbsdcfiweg
kajsbndimm s
grep
afnQWFH
ACV;SKDJNCV;
qw qde
kI UQWG
grep
grep"
while executing
"eof $file2"
It's usually a mistake to check for eof in a while loop -- check the return code from gets instead:
set filename "hello.txt"
set pattern {grep}
set count 0
set fid [open $filename r]
while {[gets $fid line] != -1} {
incr count [regexp -all -- $pattern $line]
}
close $fid
puts "$count occurrances of $pattern in $filename"
Another thought: if you're just counting pattern matches, assuming your file is not too large:
set fid [open $filename r]
set count [regexp -all -- $pattern [read $fid [file size $filename]]]
close $fid
The error message is caused by the command eof $file2. The reason is that $file2 is not a file handle (resp. channel) but contains the content of the file hello.txt itself. You read this file content with set file2 [read $file1].
If you want to do it like that I would suggest to rename $file2 into something like $filecontent and loop over every contained line:
foreach line [split $filecontent "\n"] {
... do something ...
}
Glenn is spot on. Here is another solution: Tcl comes with the fileutil package, which has the grep command:
package require fileutil
set pattern {grep}
set filename hello.txt
puts "[llength [fileutil::grep $pattern $filename]] occurrences found"
If you care about performance, go with Glenn's solution.

Parsing a file with Tcl

I have a file in here which has multiple set statements. However I want to extract the lines of my interest. Can the following code help
set in [open filename r]
seek $in 0 start
while{ [gets $in line ] != -1} {
regexp (line to be extracted)
}
Other solution:
Instead of using gets I prefer using read function to read the whole contents of the file and then process those line by line. So we are in complete control of operation on file by having it as list of lines
set fileName [lindex $argv 0]
catch {set fptr [open $fileName r]} ;
set contents [read -nonewline $fptr] ;#Read the file contents
close $fptr ;#Close the file since it has been read now
set splitCont [split $contents "\n"] ;#Split the files contents on new line
foreach ele $splitCont {
if {[regexp {^set +(\S+) +(.*)} $ele -> name value]} {
puts "The name \"$name\" maps to the value \"$value\""
}
}
How to run this code:
say above code is saved in test.tcl
Then
tclsh test.tcl FileName
FileName is full path of file unless the file is in the same directory where the program is.
First, you don't need to seek to the beginning straight after opening a file for reading; that's where it starts.
Second, the pattern for reading a file is this:
set f [open $filename]
while {[gets $f line] > -1} {
# Process lines
if {[regexp {^set +(\S+) +(.*)} $line -> name value]} {
puts "The name \"$name\" maps to the value \"$value\""
}
}
close $f
OK, that's a very simple RE in the middle there (and for more complicated files you'll need several) but that's the general pattern. Note that, as usual for Tcl, the space after the while command word is important, as is the space between the while expression and the while body. For specific help with what RE to use for particular types of input data, ask further questions here on Stack Overflow.
Yet another solution:
as it looks like the source is a TCL script, create a new safe interpreter using interp which only has the set command exposed (and any others you need), hide all other commands and replace unknown to just skip anything unrecognised. source the input in this interpreter
Here is yet another solution: use the file scanning feature of Tclx. Please look up Tclx for more info. I like this solution for that you can have several scanmatch blocks.
package require Tclx
# Open a file, skip error checking for simplicity
set inputFile [open sample.tcl r]
# Scan the file
set scanHandle [scancontext create]
scanmatch $scanHandle {^\s*set} {
lassign $matchInfo(line) setCmd varName varValue; # parse the line
puts "$varName = $varValue"
}
scanfile $scanHandle $inputFile
close $inputFile
Yet another solution: use the grep command from the fileutil package:
package require fileutil
puts [lindex $argv 0]
set matchedLines [fileutil::grep {^\s*set} [lindex $argv 0]]
foreach line $matchedLines {
# Each line is in format: filename:line, for example
# sample.tcl:set foo bar
set varName [lindex $line 1]
set varValue [lindex $line 2]
puts "$varName = $varValue"
}
I've read your comments so far, and if I understand you correctly your input data file has 6 (or 9, depending which comment) data fields per line, separated by spaces. You want to use a regexp to parse them into 6 (or 9) arrays or lists, one per data field.
If so, I'd try something like this (using lists):
set f [open $filename]
while {[gets $f line] > -1} {
# Process lines
if {[regexp {(\S+) (\S+) (\S+) (\S+) (\S+) (\S+)} $line -> name source drain gate bulk inst]} {
lappend nameL $name
lappend sourceL $source
lappend drainL $drain
lappend gateL $gate
lappend bulkL $bulk
lappend instL $inst
}
}
close $f
Now you should have a set of 6 lists, one per field, with one entry in the list for each item in your input file. To access the i-th name, for example, you grab $nameL[$i].
If (as I suspect) your main goal is to get the parameters of the device whose name is "foo", you'd use a structure like this:
set name "foo"
set i [lsearch $nameL $name]
if {$i != -1} {
set source $sourceL[$i]
} else {
puts "item $name not found."
set source ''
# or set to 0, or whatever "not found" marker you like
}
set File [ open $fileName r ]
while { [ gets $File line ] >= 0 } {
regex {(set) ([a-zA-Z0-0]+) (.*)} $line str1 str2 str3 str4
#str2 contains "set";
#str3 contains variable to be set;
#str4 contains the value to be set;
close $File
}