find and replace a number inside a file in tcl - replace

I have a huge file. I need to find the line containing the pattern abc_x and replace its value 0.34 to 10% increased value. And then copy the entire file (with replaced values) into a new file. I am new to Tcl, please let me know how to do this.
Thanks in advance.

There are three key stages to this:
Reading the old file in.
Making the update to the data in memory.
Writing the new file out.
The first and third stages are pretty standard:
set f [open input.txt]
set data [read $f]
close $f
set f [open output.txt "w"]
puts -nonewline $f $data
close $f
So now it's just about doing the transformation in memory. The best answer to this depends on the version of Tcl you're using. In all current production versions, it's probably best to split the data into lines and iterate over them, checking whether a line matches and, if it does, performing the transformation.
set lines {}
foreach line [split $data "\n"] {
if {[matchesPattern $line]} {
set line [updateLine $line]
}
lappend lines $line
}
set data [join $lines "\n"]
OK, in that code above, matchesPattern and updateLine are stand-ins for the real code, which might look like this:
if {[regexp {^(\s*abc_x\s+)([\d.]+)(.*)$} $line -> prefix value suffix]} {
# Since we matched the prefix and suffix, we must put them back
set line $prefix[expr {$value * 1.1}]$suffix
}
Composing all those pieces together gets this:
set f [open input.txt]
set data [read $f]
close $f
set lines {}
foreach line [split $data "\n"] {
if {[regexp {^(\s*abc_x\s+)([\d.]+)(.*)$} $line -> prefix value suffix]} {
set line $prefix[expr {$value * 1.1}]$suffix
}
lappend lines $line
}
set data [join $lines "\n"]
set f [open output.txt "w"]
puts -nonewline $f $data
close $f
In 8.7 you'll be able to write the update more succinctly:
set data [regsub -all -line -command {^(\s*abc_x\s+)([\d.]+)} $data {apply {{-> prefix value} {
# Since we matched the prefix, we must put it back
string cat $prefix [expr {$value * 1.1}]
}}}]
(Getting shorter than this would really require a RE engine that supports lookbehinds; Tcl's standard one does not.)

Related

Reading specific data from multiple files in Tcl

I am writing a TCL script to read multiple files and search them for a line containing certain word using regexp. I have been able to search for one thing from the files. But I need to modify the script to search for multiple things in a script print the items found in one file together in one line, then the items found from another file in 2nd line.
I have written this
foreach fileName [glob /home/kartik/tclprac/*/*] {
# puts " Directories present are: [file tail $fileName]"
set fp [open $fileName "r"]
while { [gets $fp data]>=0 } {
if {[regexp {set Date*} $data] | [regexp {set Channel* } $data] } {
#puts "file: [file dirname $fileName] data: $data"
set information "file: [file dirname $fileName] data: $data"
puts $information
set fp2 [open output.txt "a"]
puts $fp2 $information
}
}
}
Now i am getting output as:
file: /home/kartik/tclprac/wire_3 data: set Date 02/08/2021
file: /home/kartik/tclprac/wire_2 data: set Date 01/08/2021
file: /home/kartik/tclprac/wire_1 data: set Channel Disney
file: /home/kartik/tclprac/wire_1 data: set Date 31/07/2021
what i want is something like
file: /home/kartik/tclprac/wire_3 data: set Date 02/08/2021
file: /home/kartik/tclprac/wire_2 data: set Date 01/08/2021
file: /home/kartik/tclprac/wire_1 data: set Date 31/07/2021 set Channel Disney
It looks to me like you want to gather the results for a single file onto a single line, rather than printing out a line for each matching line (the traditional grep tool approach) with the per-line results being separated by spaces.
We can do this, but it gets a bit clearer if we split the code up into a couple of procedures (one for processing the contents of a single file, the other for the whole job).
proc processFileContents {name contents accumulatorChannel} {
set interesting [lmap line [split $contents "\n"] {
if {![regexp {set (?:Date|Channel) } $line]} {
# SKIP non-matching lines
continue
}
# Trim the matching lines
string trim $line
}]
# If we matched anything, print out
if {[llength $interesting]} {
set information "file: $name data: [join $interesting \n]"
puts $information
puts $accumulatorChannel $information
}
}
proc processFilesInDir {pattern accumulatorChannel} {
foreach fileName [glob -nocomplain -type f $pattern] {
set channel [open $fileName]
set contents [read $channel]
close $channel
processFileContents $fileName $contents $accumulatorChannel
}
}
set accum [open output.txt "a"]
processFilesInDir /home/kartik/tclprac/*/* $accum
close $accum
If you're using an older version of Tcl that doesn't have lmap (8.5 or before) then you can write that with foreach (as lmap is really just a collecting form of foreach; the only difference is that lmap uses a hidden temporary variable to do the accumulating):
proc processFileContents {name contents accumulatorChannel} {
set interesting {}
foreach line [split $contents "\n"] {
if {![regexp {set (?:Date|Channel) } $line]} {
# SKIP non-matching lines
continue
}
# Trim the matching lines
lappend interesting [string trim $line]
}
# If we matched anything, print out
if {[llength $interesting]} {
set information "file: $name data: [join $interesting \n]"
puts $information
puts $accumulatorChannel $information
}
}

tcl find and replace in file

I need help with tcl. I have a text file with the following format:
Main = 1
Bgp = 0
Backup = 1
I need to increment the integer value by 1 for each item, for example replacing Main = 1 with Main = 2, and so on.
Another approach:
# read the data
set f [open file]
set data [read -nonewline $f]
close $f
# increment the numbers
regsub -all {=\s*(\d+)\s*$} $data {= [expr {\1 + 1}]} new
set new [subst -novariables -nobacklashes $new]
# write the data
set f [open file w]
puts $f $new
close $f
That regsub command will replace Main = 1 with Main = [expr {1 + 1}], and then the subst command actually invokes the expr command to compute the new value
(Changed my answer: I was a little irritable, sorry.)
If the text to be processed is in a variable called data, one can turn the text into a list of words and go through them three at a time like this:
set result ""
foreach {keyword op value} [split [string trim $data]] {
append result "$keyword $op [incr value]\n"
}
In this case, each keyword (Main, Bgp, Backup, ...) ends up inside the loop variable keyword, each equals sign (or whatever is in the second position) ends up inside the loop variable op, and each value to be incremented ends up inside value.
(When splitting, it's typically a good idea to trim off white-space at the beginning and end of the text first: otherwise one can get empty "ghost" elements. Hence: split [string trim $data])
We can read the data from the file "datafile" like this:
set f [open datafile r+]
set data [read $f]
Note that we use r+ to be able to both read from and write to the file.
Once we have processed the data, we can write it back at the beginning of the file like this:
seek $f 0
puts -nonewline $f $result
close $f
or possibly like this, which means we didn't have to open the file with r+:
close $f
set f [open datafile w]
puts -nonewline $f $result
close $f
Putting it together:
set f [open datafile r+]
set data [read $f]
set result ""
foreach {keyword op value} [split [string trim $data]] {
append result "$keyword $op [incr value]\n"
}
seek $f 0
puts -nonewline $f $result
close $f
This procedure can also be simplified a bit using the standard package fileutil, which can take care of the file opening, closing, reading, and writing for us.
First, we put the processing in a procedure:
proc process data {
foreach {keyword = value} [split [string trim $data]] {
append result "$keyword ${=} [incr value]\n"
}
return $result
}
Then we can just ask ask updateInPlace to update the contents of the file with new contents processed by process.
package require fileutil
::fileutil::updateInPlace datafile process
And that's it.
Documentation:
append,
close,
fileutil (package),
foreach,
incr,
open,
package,
proc,
puts,
read,
return,
seek,
set,
split,
string

Input file needs to be edited by the code in tcl

I have a text file that I need to edit once it is input by the user.
I have used the foreach command along with split to split the input file into lines where each line breaks at \n and then I have further broken down each line into words by using split again for each line.
My objective is to replace one particular word with a new word.
And then I am supposed to replace a particular line with a new one.
How do I do so?
You don't really need to split the contents of the file at all:
set fh [open deck r]
set deck [read -nonewline $fh]
close $fh
regsub -all {\mivfrz\M} $deck {i-v} deck
I use the \m and \M constraints to ensure that I'm only matching a whole word. See http://tcl.tk/man/tcl8.6/TclCmd/re_syntax.htm#M72
To rewrite your method:
foreach line [split $deck \n] {
set words [split $line]
foreach index [lsearch -all -exact $words "ivfrz"] {
lset words $index "i-v"
}
lappend new_deck $words
}

Print lines before and after matching regexp in TCL

I want to be able to print 10 lines before and 10 lines after I come across a matching pattern in a file. I'm matching the pattern via regex. I would need a TCL specific solution. I basically need the equivalent of the grep -B 10 -A 10 feature.
Thanks in advance!
If the data is “relatively small” (which can actually be 100MB or more on modern computers) then you can load it all into Tcl and process it there.
# Read in the data
set f [open "datafile.txt"]
set lines [split [read $f] "\n"]
close $f
# Find which lines match; adjust to taste
set matchLineNumbers [lsearch -all -regexp $lines $YourPatternHere]
# Note that the matches are already in order
# Handle overlapping ranges!
foreach n $matchLineNumbers {
set from [expr {max(0, $n - 10)}]
set to [expr {min($n + 10, [llength $lines] - 1)}]
if {[info exists prev] && $from <= $prev} {
lset ranges end $to
} else {
lappend ranges $from $to
}
set prev $to
}
# Print out the ranges
foreach {from to} $ranges {
puts "=== $from - $to ==="
puts [join [lrange $lines $from $to] "\n"]
}
The only mechanism that springs to mind is for you to split the input data into a list of lines. You'd then need to sweep through the list and whenever you found a match output a suitable collection of entries from the list.
To the best of my knowledge there's no built-in, easy way of doing this.
There might be something useful in tcllib.
I'd use grep myself.

How to change lower case to upper case in Tcl?

I am trying to regsub all lower case letters to upper case in a file using character classes:
regsub -all { [:lower:] } $f { [:upper:] } f
but it doesn't do the substitution.
Just read the file into a string and use string toupper. Then write it back out to a file.
set fp [open "somefile" r]
set file_data [read $fp]
close $fp
set file_data [string toupper $file_data]
set fp [open "somefile" "w"]
puts -nonewline $fp $file_data
close $fp
yes, above will work like charm.
set f [string toupper $f]
f is some list or string. If you want file operations, as usual read from file and write .
Although if you just want to use regsub, give this a try
set f "this is a line"
regsub -all {.*} $f {[string toupper {&}]} f
set f [subst -nobackslashes -novariables $f]
now your contents in f is uppercased.
note: it looks like long way but useful when selecting just particular text to be upper or lowercased
Thanks,