example code to make puts log the output - tcl

I'm looking for some Tcl code that would duplicate what puts command sends to stdout to some log file. Yes, there is a possibility to change all calls to puts to some custom function. But I would like to make it as transparent as possible.
I have this trial code, but it doesn't really work that well:
set pass_log_output "0"
rename puts _puts
proc puts { args } {
global pass_log_output
if {[info exists pass_log_output]} {
# There can be several cases:
# -nonewline parameter, stdout specified or not
set stdout_dest [ lsearch $args stdout ]
set nonewline [ lsearch $args -nonewline ]
if { $stdout_dest != -1 } {
log_low_level "" [lindex $args [expr $stdout_dest + 1]] ""
} elseif { $nonewline != -1 && [ llength $args ] > 1} {
log_low_level "" [lindex $args [expr $nonewline + 1]] ""
} else {
log_low_level "" [lindex $args 0] ""
}
}
if { [ catch { eval _puts $args } err ] } {
return -code error $err
}
}
log_low_level function just stores the passed string in a file.
So far I'm getting this error:
Tcl Interpreter Error: too many nested evaluations (infinite loop?)

Does log_low_level use puts? That could be your infinite loop.
If so, try changing it to use _puts.

Thanks for the points. I just want to post the final working code for reference. It even takes care of the storing lines with -nonewline flag properly.
set pass_log_output "0"
set last_call_nonewline 0
rename puts _orig_puts
proc puts { args } {
global pass_log_output
global g_log_file
global last_call_nonewline
if {[info exists pass_log_output]} {
# Check if the logging was initialized
if {![info exists g_log_file]} {
_orig_puts "Log file wasn't initialized!"
return
}
# There can be several cases:
# -nonewline parameter, stdout specified or not
set stdout_dest [ lsearch $args stdout ]
set nonewline [ lsearch $args -nonewline ]
if {[ llength $args ] > 3} {
return -code error "wrong # args: should be puts ?-nonewline? ?channelId? string"
} elseif { $stdout_dest != -1 } {
set message [lindex $args end]
} elseif { $nonewline != -1 && [ llength $args ] == 2} {
set message [lindex $args [expr $nonewline + 1]]
} elseif {[ llength $args ] == 1} {
set message [lindex $args 0]
}
# Store the message in the file, if needed.
# Take into account if the last call was with -nonewline
if {[info exists message]} {
if {$last_call_nonewline == 0} {
_orig_puts -nonewline $g_log_file [clock format [clock seconds] -format "%T - "]
}
if {$nonewline != -1} {
set last_call_nonewline 1
_orig_puts -nonewline $g_log_file "$message"
} else {
set last_call_nonewline 0
_orig_puts $g_log_file "$message"
}
flush $g_log_file
}
}
if { [ catch { eval _orig_puts $args } err ] } {
return -code error $err
}
}

Since puts has very few options, it may be easier to consider the number of args given. Also, you should contain all uses of the original _puts to your new puts proc -- this new puts should be transparent even to your code.
I assume you only want to log stuff you're writing to stdout
rename puts _orig_puts
proc puts {args} {
switch -exact [llength $args] {
3 {
# both -newline and a channelId are given
set do_log [expr {[lindex $args 1] eq "stdout"}]
}
2 {
# only log if not writing to stdout
set chan [lindex $args 0]
set do_log [expr {$chan eq "-nonewline" || $chan eq "stdout"}]
}
1 {
set do_log true
}
default {
error {wrong # args: should be "puts ?-nonewline? ?channelId? string"}
}
}
if {$do_log} {
set chan [open $::mylogfile a]
_orig_puts $chan [lindex $args end]
close $chan
}
_orig_puts {*}$args
}

Related

Accessing variables in TCL across scopes

I'm trying to learn tcl scripting. My req is very simple. I need to access the array "args" in the second if condition in the for loop. I tried the code below. Since "argv" scope is limited to second if condition, it is NOT accessible in for loop
Then I tried declaring argv as global var -
array set args {}
right below the ned of first if condition. Even after declaring "args" as global array did NOT help.
How do I access the variable in the cope of second if contion, in the for loop below ?
if {$argc != 4} {
puts "Insufficient arguments"
exit 1
}
if { $::argc > 0 } {
set i 1
foreach arg $::argv {
puts "argument $i is $arg"
set args(i) arg
incr i
}
} else {
puts "no command line argument passed"
}
for {set x 0} { $x<2 } {incr x} {
puts "Arrray: [lindex $args $x]"
}
For your original code, this is the error I get:
can't read "args": variable is array
while executing
"lindex $args $x"
("for" body line 2)
invoked from within
"for {set x 0} { $x<2 } {incr x} {
puts "Arrray: [lindex $args $x]"
}"
(file "main.tcl" line 20)
In Tcl, arrays are not lists. You have to write
for {set x 0} { $x<2 } {incr x} {
puts "Arrray: $args($x)"
}
But then I get this:
can't read "args(0)": no such element in array
while executing
"puts "Arrray: $args($x)""
("for" body line 2)
invoked from within
"for {set x 0} { $x<2 } {incr x} {
puts "Arrray: $args($x)"
}"
(file "main.tcl" line 20)
Well there's several problems here. You're setting array elements starting with index 1 but reading them starting with index 0. So let's correct that to 0 everywhere:
set i 0
But also you're missing some $'s in the setting of the elements:
set args($i) $arg
That looks better. Final code:
if {$argc != 4} {
puts "Insufficient arguments"
exit 1
}
if { $::argc > 0 } {
set i 0
foreach arg $::argv {
puts "argument $i is $arg"
set args($i) $arg
incr i
}
} else {
puts "no command line argument passed"
}
for {set x 0} { $x<2 } {incr x} {
puts "Arrray: $args($x)"
}
So, scope wasn't quite the issue. You're getting there though!
Tcl does not import globals by default. You need to import your globals:
global args
set args(i) arg
Some people prefer to import globals at the top of the proc:
global args
if {$argc != 4} {
puts "Insufficient arguments"
exit 1
}
if { $::argc > 0 } {
set i 1
....
See: https://www.tcl.tk/man/tcl8.7/TclCmd/global.htm
Alternatively, you can directly access the global namespace, in fact you're already using that syntax with ::argc:
set ::args(i) arg

chown from query bind msg

how can set from qry .chanset ?
bind msg - .flag msg:flag
proc msg:flag {nick uhost handle text} {
if {$text == ""} {
putnow "PRIVMSG $nick :.flag #channel on/off"
return 0
}
set chan [lindex [split $text] 0]
set button [lindex [split $text] 1]
set flag [lindex [split $text] 2]
if { [string match -nocase "on" "$button"] } {
chanset $chan +$flag
}
if { [string match -nocase "off" "$button"] } {
chanset $chan -$flag
}
putserv "PRIVMSG $nick :Change Flag Successful."
}
error msg on partyline :
[18:12:50] Tcl error: invalid command name "chanset"

How to unset an arg in tcl

I am taking arguments from command line and passing all those arguments to another program (with expect spawn). I want to parse all options and omit some of them (or do something else). To do that I am doing this:
set arguments [lrange $argv 0 end]
#Check for -lp option. Set the program path
for {set var 0} {$var<$argc} {incr var} {
if {[lindex $arguments $var] == "-lp" || [lindex $arguments $var] == "--launcher-path"} {
if {[lindex $arguments [expr {$var+1}]] != ""} {
set program [lindex $arguments [expr {$var+1}]]
#unset [lindex $arguments $var]
} else {
puts "E: Argument missing for option: [lindex $arguments $var]"
exit 1
}
}
}
But I can't figure out how to unset those args that I used. For example, I need to unset [lindex $arguments [expr {$var+1}]] and [lindex $arguments $var].
This is how I am running the $program:
if {[catch {spawn $program --text {*}$arguments}]} {
puts "E: Launcher not found: $program"
exit 1
}
If your arguments are all key-value, then you can iterate over the arguments in pairs with foreach and build up a new list containing just the arguments you're interested in.
set newarguments [list]
foreach {arg value} $arguments {
switch -exact -- $arg {
"-lp" -
"--launcher-path" {
set program $value
}
default {
lappend newarguments $arg $value
}
}
}
If you have mixed flag and key-value options, then you will need to iterate using an index, similar to your code, but building up the new list of arguments will be roughly the same.
You could also check into the tcllib cmdline package, although that does not handle long options.
This is how I have done it:
set arguments [lreplace $arguments [expr {$var+1}] [expr {$var+1}]]
set arguments [lreplace $arguments $var $var]
As glenn-jackman pointed out, the above can be shortened to:
set arguments [lreplace $arguments $var [expr {$var+1}]]

How to parse txt file containing a repository of patterns

I am new in scripting in TCL, I want to parse a txt file to create a list of patterns based on 2 strings as input.
My file looks like:
keyw1: data1
keyw1: data2
keyw1: Arg1
:
:
keyword: Pattern2Extract
{
some_lines
keyw1: Arg1
keyw2: patternP1
{
some_lines
}
keyw2: Arg2
{
some_lines
}
keyw2: patternP2
{
some_lines
}
.
.
some_others blocks of declaration between braces {}
.
.
}
keyword: Pattern2Extract
{
some_lines
keyw1: Arg1
keyw2: Arg2
{
some_lines
}
keyw2: patternP1
{
some_lines
}
keyw2: patternP2
{
some_lines
}
.
.
some_others blocks of declaration between braces {}
.
.
}
So, I would like to output 2 list of 'Pattern2Extract'
list1: if Arg1 is found in structure grouped between curly braces {}
list2: if arg1 and arg2 are both in structure grouped between curly braces {}
I have tried lsearch and lindex and it's working for list1 but I don't know how to do it for list2.
Here is my script:
proc inst_nm {inpFile outFile} {
set chanId [open $inpFile r]
set data [list]
while {[gets $chanId line] != -1} {
lappend data $line
}
close $chanId
foreach dt $data {
set MasDat [lindex $dt 0]
set pinDat [lindex $dt 1]
}
set intId [open "./filetoparse.txt" r]
set instDat [list]
while {[gets $intId line] != -1} {
lappend instDat $line
}
close $intId
set writeId [open $outFile a]
set MasterList [lreplace [lsearch -all $instDat *$MasDat*] 0 0]
foreach elem $MasterList {
set cellLn [lindex [split [lindex $instDat $elem ] ":"] 1]
set instName [lindex [split [lindex $instDat [expr $elem -5]] ":"] 1]
set PinLn [lindex [split [lindex $instDat [expr $elem +1]] ":"] 1]
foreach ele $PinLn {
if {"$ele"=="$pinDat" } {
puts $writeId "$instName $pinDat $cellLn"
} else {
puts $writeId "$instName $ele $cellLn"
}
}
}
close $writeId
}
inst_nm [lindex $::argv 0] [lindex $::argv 1]
Currently, inpFile may have many lines like $MastDat $pinDat and I need to collect instDat corresponding to each pair ($MastDat,$pinDat).
in file_to_parse by construction, we know that instName come in fifth line before $MastDat. However, we don't know the position of line conatining $pinDat declaration and this pattern could be present or not into instance section:
keyword: Pattern2Extract { some_lines keyw1: Arg1 keyw2: patternP1 { some_lines } keyw2: Arg2 { some_lines } keyw2: patternP2 { some_lines } . . some_others blocks of declaration between braces {} . . }
so, in list2 we should get all insName in where $pinDat is found
Thank you for your help
It helps to break out the code into another proc. In Tcl the proc must be declared ahead of when you call it. The data file didn't reflect your parser and also the MasterList might be removing the found item your looking for. Below is your parser broken up with example files that reflect what it's doing.
#!/usr/bin/tclsh
proc findPin {MasDat pinDat instDat} {
# set MasterList to the list of indexes found for *$MastDat*
set MasterList [lsearch -glob -all $instDat *$MasDat*]
set found [list]
# for each index number in MasterList
foreach elem $MasterList {
# n-5 (key: value(instName))
# n-4
# n-3
# n-2
# n-1
# n (key: value(cellLn)
# n+1 (key: value(PinLn)
set cellLn [lindex [split [lindex $instDat $elem ] ":"] 1]
set instName [lindex [split [lindex $instDat [expr $elem -5]] ":"] 1]
set PinLn [lindex [split [lindex $instDat [expr $elem +1]] ":"] 1]
foreach ele $PinLn {
if {"$ele"=="$pinDat" } {
lappend found "$instName $pinDat $cellLn"
}
}
}
return $found
}
proc inst_nm {inpFile outFile} {
# geta all lines in filestoparse.txt
set intId [open "./filetoparse.txt" r]
set instDat [list]
while {[gets $intId line] != -1} {
lappend instDat $line
}
close $intId
set writeId [open $outFile a]
# Search each line in inpFile
set chanId [open $inpFile r]
while {[gets $chanId line] != -1} {
set MasDat [lindex $line 0]
set pinDat [lindex $line 1]
foreach {item} [findPin $MasDat $pinDat $instDat] {
puts $writeId $item
}
}
close $chanId
close $writeId
}
inst_nm [lindex $::argv 0] [lindex $::argv 1]
filetoparse.txt
INST_NAME:MyInst
unknown-1
unknown-2
unknown-3
unknown-4
CELL_LN:MyCellLn
PIN_LN:pin1 pin2 pin3 pin4 pin5
unknown...
INST_NAME:TestInst
unknown-1
unknown-2
unknown-3
unknown-4
CELL_LN:TestCell
PIN_LN:test1 test2 test3
inputfile.txt
MyCellLn pin4
MyCellLn pin25
TestCell test1
TestCell test10
MyCellLn pin3
Output:
% ./keylist.tcl inputfile.txt keylist_found.txt
% cat keylist_found.txt
MyInst pin4 MyCellLn
TestInst test1 TestCell
MyInst pin3 MyCellLn
Actually, I'm interested just by printing '$instName' for each pair line from inpFile '$cellLn $pinDat'
filetoparse.txt:
INST_NAME:Inst1
{
4 unknown lines
CELL_LN: Cell1
other unkown lines
PIN_LN:pin1
unkown
PIN_LN:pin5
unknown...
}
INST_NAME:Inst2
{
4 unknown lines
CELL_LN: Cell1
other unkown lines
PIN_LN:pin3
unkown
PIN_LN:pin5
unknown...
}
INST_NAME:Inst3
{
4 unknown lines
CELL_LN: Cell2
other unkown lines
PIN_LN:pin2
unkown
PIN_LN:pin4
unknown...
}
INST_NAME:Inst4
{
4 unknown lines
CELL_LN: Cell2
other unkown lines
PIN_LN:pin5
unkown
PIN_LN:pin2
unknown...
}
inpFile.txt
cell1 pin1
cell2 pin2
So, I want in OutputFile have something like:
- for cell1 pin1:
list1: {Inst1 Inst2}
list2: {Inst1}
- for cell2 pin2:
list1: {Inst3 Inst4}
list2: {Inst3 Inst4}
Thank you for your help,

How can i specify switches on command line in tcl?

If i have an expect script and i want to execute certain part of code depending on the requirement.suppose if i have some procedures in my code like below
proc ABLOCK { } {
}
proc BBLOCK { } {
}
proc CBLOCK { } {
}
then while executing the script if i can use some switches like.
./script -A ABLOCK #executes only ABLOCK
./script -A ABLOCK -B BBLOCK #executes ABLOCK and BBLOCK
./script -V # just an option for say verbose output
where ABLOCK,BBLOCK,CBLOCK could be list of args argv
Why not:
foreach arg $argv {
$arg
}
and run it as ./script ABLOCK BLOCK CBLOCK
Someone could also pass exit, if you don't want that, check if it is valid:
foreach arg $argv {
if {$arg in {ABLOCK BLOCK CBLOCK}} {
$arg
} else {
# What else?
}
}
For switches, you could to the same (if they don't require a parameter):
proc -V {} {
set ::verbose 1
# Enable some other output
}
If you need arguments to switches, you could do the following:
set myargs $argv
while {[llength $myargs]} {
set myargs [lassign $myargs arg]
if {[string index $arg 0] eq {-}} {
# Option
if {[string index $arg 1] eq {-}} {
# Long options
switch -exact -- [string range $arg 2 end]
verbose {set ::verbose 1}
logfile {set myargs [lassign $myargs ::logfile]}
}
} else {
foreach opt [split [string range $arg 1 end] {}] {
switch -exact $opt {
V {set ::verbose 1}
l {set myargs [lassign $myargs ::logfile]}
}
}
}
} else {
$arg
}
}