append a string in a file via tcl - file

i want to open up a pre-existed file and want to add a string inside the file one line before it sees the word 'exit' inside the file. the word 'exit' will always be the last line inside the file, so we can also see this as " add the string one line above the last line" problem. in other words, I want to append this string inside the file. here is example
Example.tcl (before)
AAAAAAA
BBBBBBB
CCCCCC
exit
Example.tcl (after)
AAAAAAA
BBBBBBB
CCCCCC
new_word_string
exit
Any suggestions are most welcome.

Working code:
Open the file for reading, and also open a temporary file:
set f1 [open $thefile]
set f2 [file tempfile]
Read one line at a time until all lines have been read. Look at the line. If it is the string "exit", print the new string to the temporary file. The write the line you read to the temporary file.
while {[set line [chan gets $f1]] ne {}} {
if {$line eq "exit"} {
chan puts $f2 $thestring
}
chan puts $f2 $line
}
Close the file and reopen it for reading.
chan close $f1
set f1 [open $thefile w]
Rewind the temporary file to the start position.
chan seek $f2 0
Read the entire contents of the temporary file and print them to the file.
chan puts -nonewline $f1 [chan read -nonewline $f2]
Close both files.
chan close $f1
chan close $f2
And we're done.
You could use a string buffer instead of a temporary file with minimal changes, to wit:
set f [open $thefile]
set tempstr {}
while {[set line [chan gets $f]] ne {}} {
if {$line eq "exit"} {
append tempstr $thestring\n
}
append tempstr $line\n
}
chan close $f
set f [open $thefile w]
chan puts -nonewline $f $tempstr
chan close $f
Documentation: append, chan, if, open, set, while

You could farm the work out to an external command (Tcl was written as a glue language after all):
% exec cat example.tcl
AAAAAAA
BBBBBBB
CCCCCC
exit
% set new_line "this is the new line inserted before exit"
this is the new line inserted before exit
% exec sed -i "\$i$new_line" example.tcl
% exec cat example.tcl
AAAAAAA
BBBBBBB
CCCCCC
this is the new line inserted before exit
exit

Related

Tcl strings to middle of binary file without overwriting its conten

I have a binary file
in which I am trying to add a string in the middle of the file
(lets say after 10 Bytes)
I succees to overwrite the file with my string - but not to append
appreciate if someone can tell how can I append the string.
Here is my code example:
proc write_bit_header {} {
set bit_hdr "#Here is my new string to be added#"
set bit_hdr_len [string length ${bit_hdr}]
set outBinData [binary format a${bit_hdr_len} ${bit_hdr}]
set fp [open "binfile" "a+b"]
fconfigure $fp -translation binary
seek $fp 10
puts -nonewline $fp $outBinData
close $fp
}
When you write to the middle of a file (which you'd use the mode r+b for), none of the other bytes in the file move around. They're still at exactly the same offsets within the file that they were beforehand. If you're writing a fixed-size binary record into the file, this is exactly what you want! However, if you're writing a variable sized record, you have to:
read all the data that is going to go after the bytes that you want to write
seek to the place where you want to do the insert/replace
write the data that you are inserting
write the data that you read in step 1
truncate the file (in case what you wrote in step 3 is shorter than what you were replacing).
Yes, this is non-trivial!
proc insertData {filename dataToInsert insertionPoint {firstAfterByte ""}} {
# If you don't give the end of the range to overwrite, it's zero-length
if {$firstAfterByte eq ""} {
set firstAfterByte $insertionPoint
}
set f [open $filename "r+b"]
chan seek $f $firstAfterByte
set suffixData [chan read $f]
chan seek $f $insertionPoint
chan puts -nonewline $f $dataToInsert
chan puts -nonewline $f $suffixData
chan truncate $f
close $f
}
It's much easier when you're appending, as you are not having to move around any existing data and never need to truncate. And you can use the ab mode so that you don't need to seek explicitly.
proc appendData {filename dataToAppend} {
set f [open $filename "ab"]
puts -nonewline $f $dataToAppend
close $f
}
As you can see, the insertion code is quite a lot more tricky. It runs quite a bit of a risk of going wrong too. It's better to use a working copy file, and then replace the original at the end:
proc insertDataSafely {filename dataToInsert insertionPoint {firstAfterByte ""}} {
set f_in [open $filename "rb"]
set f_out [open ${filename}.tmp "wb"]
try {
chan copy $f_in $f_out $insertionPoint
puts -nonewline $f_out $dataToInsert
if {$firstAfterByte ne ""} {
chan seek $f_in $firstAfterByte
}
chan copy $f_in $f_out
chan close $f_in
chan close $f_out
} on ok {} {
file rename ${filename}.tmp $filename
} on error {msg opt} {
file delete ${filename}.tmp
# Reraise the error
return -options $opt $msg
}
}
Of course, not all files take kindly to this sort of thing being done in the first place, but the ways in which modifying an arbitrary file can make things go haywire is long and thoroughly out of scope for this question.

How can I redirect the powershell output to my tcl-file?

Inside a .tcl file the batch file "test.ps1" is executed.
set output [exec test.ps1]
puts $output
Edit:
When I simply execute directly the test.ps1 file I can see all outputs in a shell-window. If I call the .tcl file I do not see the output only at the end when the batch file finished. All output text is written at the end but not updated when the ps1-file is still running.
What I see is that the application, where the .tcl file is called, goes to a "freeze" state so it is not possible to use that GUI as long the .ps1 file is running. At the end of the ps1 file all output is written at once and I can use the application again.
Question:
Is there a way to continously update the shell window in order to see the output?
You can execute the command in background using & and redirect its output to the tcl script output using >#stdout:
exec test.ps1 >#stdout &
Check the Tcl exec docs for details on how this works.
What about this:
################################################################################
##### Calls a single Powershell command (blocking, hidden)
### Arg: The command to give to Powershell via -command switch
### Ret: A List of three elements:
### -1 "" <errtext> -> error from twapi::create_process
### 0 <stdouttxt> "" -> Ok
### 1 "..." <stderrtext> -> Maybe Ok, something written to stderr
#
proc execPowershellCmd {cmd} {
set cmd "-command $cmd"
foreach chan {stdin stdout stderr} {
lassign [chan pipe] rd$chan wr$chan
}
if {[catch {
set cmd [string map [list \" \\\"] $cmd]; # muss noch in Wiki...
twapi::create_process [auto_execok powershell] -cmdline $cmd -showwindow hidden \
-inherithandles 1 -stdchannels [list $rdstdin $wrstdout $wrstderr]
} ret]} {
return [list -1 "" $ret]
}
chan close $wrstdin; chan close $rdstdin; chan close $wrstdout; chan close $wrstderr
foreach chan [list $rdstdout $rdstderr] {
chan configure $chan -encoding cp850 -blocking true; # -buffering full?; # -enc?
}
set out [read $rdstdout]; set err [read $rdstderr]
chan close $rdstdout; chan close $rdstderr
return [list [string compare $err ""] $out $err]
}

how to read a large file line by line using tcl?

I've written one piece of code by using a while loop but it will take too much time to read the file line by line. Can any one help me please?
my code :
set a [open myfile r]
while {[gets $a line]>=0} {
"do somethig by using the line variable"
}
The code looks fine. It's pretty quick (if you're using a sufficiently new version of Tcl; historically, there were some minor versions of Tcl that had buffer management problems) and is how you read a line at a time.
It's a little faster if you can read in larger amounts at once, but then you need to have enough memory to hold the file. To put that in context, files that are a few million lines are usually no problem; modern computers can handle that sort of thing just fine:
set a [open myfile]
set lines [split [read $a] "\n"]
close $a; # Saves a few bytes :-)
foreach line $lines {
# do something with each line...
}
If it truly is a large file you should do the following to read in only a line at a time. Using your method will read the entire contents into ram.
https://www.tcl.tk/man/tcl8.5/tutorial/Tcl24.html
#
# Count the number of lines in a text file
#
set infile [open "myfile.txt" r]
set number 0
#
# gets with two arguments returns the length of the line,
# -1 if the end of the file is found
#
while { [gets $infile line] >= 0 } {
incr number
}
close $infile
puts "Number of lines: $number"
#
# Also report it in an external file
#
set outfile [open "report.out" w]
puts $outfile "Number of lines: $number"
close $outfile

How can I redirect stdout into a file in tcl

how can I redirect a proc output into a file in tcl, for example, I have a proc foo, and would like to redirect the foo output into a file bar. But got this result
% proc foo {} { puts "hello world" }
% foo
hello world
% foo > bar
wrong # args: should be "foo"
If you cannot change your code to take the name of a channel to write to (the most robust solution), you can use a trick to redirect stdout to a file: reopening.
proc foo {} { puts "hello world" }
proc reopenStdout {file} {
close stdout
open $file w ;# The standard channels are special
}
reopenStdout ./bar
foo
reopenStdout /dev/tty ;# Default destination on Unix; Win equiv is CON:
Be aware that if you do this, you lose track of where your initial stdout was directed to (unless you use TclX's dup to save a copy).
This should work:
% proc foo {} { return "hello world" }
% foo
hello world
% exec echo [foo] > bar
% exec cat bar
hello world
% exec echo [foo] >> bar
% exec cat bar
hello world
hello world
%
Generally, I'd recommend the stdout redirection that Donal Fellows suggests in his answer.
Sometimes this may not possible. Maybe the Tcl interpreter is linked into a complex application that has itself a fancy idea of where the output should go to, and then you don't know how to restore the stdout channel.
In those cases you can try to redefine the puts command. Here's a code example on how you could do that. In plain Tcl, a command can be redefined by renaming it into a safe name, and creating a wrapper proc that calls the original command at the safe name - or not at all, depending on your intended functionality.
proc redirect_file {filename cmd} {
rename puts ::tcl::orig::puts
set mode w
set destination [open $filename $mode]
proc puts args "
uplevel \"::tcl::orig::puts $destination \$args\"; return
"
uplevel $cmd
close $destination
rename puts {}
rename ::tcl::orig::puts puts
}
You can also redirect text into a variable:
proc redirect_variable {varname cmd} {
rename puts ::tcl::orig::puts
global __puts_redirect
set __puts_redirect {}
proc puts args {
global __puts_redirect
set __puts_redirect [concat $__puts_redirect [lindex $args end]]
set args [lreplace $args end end]
if {[lsearch -regexp $args {^-nonewline}]<0} {
set __puts_redirect "$__puts_redirect\n"
}
return
}
uplevel $cmd
upvar $varname destination
set destination $__puts_redirect
unset __puts_redirect
rename puts {}
rename ::tcl::orig::puts puts
}
The Tcl'ers Wiki has another interesting example of redefining puts in more complex applications. Maybe this is inspiring as well.
At present when you type foo > bar Tcl is trying to run a foo proc that takes 2 parameter, as this doesn't exist you get the error message. I can think of two ways you could tackle this problem.
You can redirect at the top level, so when you run tclsh tclsh > bar then all of the output will be redirected, however I doubt this is what you want.
You could change foo so that it accepts an open file as a parameter and write to that:
proc foo {fp} {
puts $fp "some text"
}
set fp [open bar w]
foo $fp
close $fp
To accomplish this I wrapped the call to my Tcl proc in a shell script. This works on unix, not sure about other OS.
file - foo.tcl:
proc foo {} {puts "Hi from foo"}
file - foo.csh (any shell script will work, I'm using csh for this example):
enter code here#!/usr/bin/tclsh
source foo.tcl
eval "foo $argv"
file - main.tcl:
exec foo.csh > ./myoutput.txt
Of course these commands can be made 'fancy' by wrapping them in safety measures like catch, etc... for clarity sake I didn't include them, but I would recommend their use. Also I included $argv which isn't needed since proc foo doesn't take args, but typically IRL there will be args. Be sure to use >> if you just want to append to the file rather than overwriting it.
Thanks for sharing CFI.
I would like to contribute as well.
so, i made some changes to redefine puts on dump to file on startlog and end logging on endlog to file when ever we want.
This will print on stdout and also redirect stdout to file.
proc startlog {filename } {
rename puts ::tcl::orig::puts
set mode w
global def destination logfilename
set destination [open $filename $mode]
set logfilename $filename
proc puts args "
uplevel 2 \"::tcl::orig::puts \$args\"
uplevel \"::tcl::orig::puts $destination \{\$args\}\"; return
"
}
proc endlog { } {
global def destination logfilename
close $destination
rename puts {}
rename ::tcl::orig::puts puts
cleanlog $logfilename
puts "name of log file is $logfilename"
}
proc cleanlog {filename } {
set f [open $filename]
set output [open $filename\.log w]
set line1 [regsub -all {\-nonewline stdout \{} [read $f] ""]
set line2 [regsub -all {stdout \{} $line1 ""]
set line3 [regsub -all {\}} $line2 ""]
set line4 [regsub -all {\{} $line3 ""]
puts $output $line4
close $output
file rename -force $filename.log $filename
}
We can try in this way also
% proc foo {} { return "hello world" }
% foo
hello world
% set fd [open "a.txt" w]
file5
% set val [foo]
hello world
% puts $fd $val
% close $fd
% set data [exec cat a.txt]
hello world
you can directly use the command "redirect"

in tcl, how do I replace a line in a file?

let's say I opened a file, then parsed it into lines. Then I use a loop:
foreach line $lines {}
inside the loop, for some lines, I want to replace them inside the file with different lines. Is it possible? Or do I have to write to another temporary file, then replace the files when I'm done?
e.g., if the file contained
AA
BB
and then I replace capital letters with lower case letters, I want the original file to contain
aa
bb
Thanks!
for plain text files, it's safest to move the original file to a "backup" name then rewrite it using the original filename:
Update: edited based on Donal's feedback
set timestamp [clock format [clock seconds] -format {%Y%m%d%H%M%S}]
set filename "filename.txt"
set temp $filename.new.$timestamp
set backup $filename.bak.$timestamp
set in [open $filename r]
set out [open $temp w]
# line-by-line, read the original file
while {[gets $in line] != -1} {
#transform $line somehow
set line [string tolower $line]
# then write the transformed line
puts $out $line
}
close $in
close $out
# move the new data to the proper filename
file link -hard $filename $backup
file rename -force $temp $filename
In addition to Glenn's answer. If you would like to operate on the file on a whole contents basis and the file is not too large, then you can use fileutil::updateInPlace. Here is a code sample:
package require fileutil
proc processContents {fileContents} {
# Search: AA, replace: aa
return [string map {AA aa} $fileContents]
}
fileutil::updateInPlace data.txt processContents
If this is Linux it'd be easier to exec "sed -i" and let it do the work for you.
If it's a short file you can just store it in a list:
set temp ""
#saves each line to an arg in a temp list
set file [open $loc]
foreach {i} [split [read $file] \n] {
lappend temp $i
}
close $file
#rewrites your file
set file [open $loc w+]
foreach {i} $temp {
#do something, for your example:
puts $file [string tolower $i]
}
close $file
set fileID [open "lineremove.txt" r]
set temp [open "temp.txt" w+]
while {[eof $fileID] != 1} {
gets $fileID lineInfo
regsub -all "delted information type here" $lineInfo "" lineInfo
puts $temp $lineInfo
}
file delete -force lineremove.txt
file rename -force temp.txt lineremove.txt
For the next poor soul that is looking for a SIMPLE tcl script to change all occurrences of one word to a new word, below script will read each line of myfile and change all red to blue then output the line to in a new file called mynewfile.
set fin "myfile"
set fout "mynewfile"
set win [open $fin r]
set wout [open $fout w]
while {[gets $win line] != -1} {
set line [regsub {(red)} $line blue]
puts $wout $line
}
close $win
close $wout

Resources