Compilation Error when running Echo Service example from Tcl Book - tcl

Doing the echo service example in the book, 'Practical Programming in Tcl & TK 4th edition' Brent B. Welch Ken Jones Jeffrey Hobbs.
Its on page 241, example 17-3. Copied it straight out of the book and its giving me the following error:
tclsh "theEchoService.tcl" (in directory: /home/<username>/Documents/Scripts/tcl)
Compilation failed.
wrong # args: should be "proc name args body"
while executing
"proc Echo {sock} \
{
global echo
if {[eof $sock]} || [catch {gets $sock line}]} \
{
;# end of file or abnormal connection drop
close $sock
pu..."
(file "theEchoService.tcl" line 16)
Heres my full code:
#!/usr/bin/tclsh
;#The Echo Service. Socket ProgrammingPage 241, Example 17-3
proc Echo_Server {port} \
{
global echo
set echo(main) [socket -server EchoAccept $port]
}
proc EchoAccept {sock addr port} \
{
global echo
puts "Accept $sock from $addr $port"
set echo(addr, $sock) [list $addr $port]
fconfigure $sock -buffering line
fileevent $sock readable [list Echo $sock]
}
proc Echo {sock} \
{
global echo
if {[eof $sock]} || [catch {gets $sock line}]} \
{
;# end of file or abnormal connection drop
close $sock
puts "Close $echo(addr, $sock)"
unset echo(addr,$sock)
} \
else \
{
if {[string compare $line "quit"] == 0} \
{
;# Prevent new connections, Existing connections stay open
close $echo(main)
}
puts $sock $line
}
}
I've tried it without my escapes and still the same. Any ideas?

The problem is this line:
if {[eof $sock]} || [catch {gets $sock line}]} \
^^^
That extra } is terminating the if early, which makes the } at the end of the line terminate the body of the procedure early (and the rest of what you think the body is appears as extra arguments to proc, which doesn't like it).
You're recommended to avoid using backslashes to introduce newlines like that; it's just extra visual noise. You're also recommended to use an editor which can do auto-indentation and/or bracket matching, both of which would have helped you find your problem virtually immediately.

Don't try and make your Tcl code look like C. It is not the same language at all. Every statement in Tcl is made up of words terminated by a newline or semicolon. One way to group words is using the curly braces but these need to be on the same line as the earlier part of the phrase. You can escape the newline as it looks like you are attempting to do but this is fragile and hard to maintain because if you introduce any whitespace after your escape character you no longer escape the newline and you get the error you are seeing.
In Tcl code, put the opening braces on the same line as the code. eg:
proc Echo {sock} {
if {1 == 0} {
puts "something"
} else {
puts "do something else"
expr {
1 * 2 +
3
}
}
}
This is explained in detail in the Tcl(1) manual page but you have to read it rather carefully to glean the details.

Related

How to check the string value of an argument passed to expect script?

I'm writing a expect script that takes command line arguments. I would like to be able to detect whether the first argument is "--help" and print a Usage string then. Otherwise use the argument as a port number with a specific default (let's say 1818).
I tried this code that fails:
#!/usr/bin/expect
if {[llength $argv] != 1} {
puts "No Port number specified, defaulting to port 1818."
set port 1818
} else {
if {[lindex $argv 0] eq "--help"} {
puts "Usage: testit [--help] [port]"
exit
} else {
set port [lindex $argv 0]
}
}
The error is:
invalid command name "--help"
while executing
"--help"
invoked from within
"if {[llength $argv] != 1} {
puts "No Port number specified, defaulting to port 1818."
set port 1818
} else {
if {[lindex $argv 0] eq "--he..."
Obviously it is trying to interpret the content of the "--help" string while I'm trying to make the script compare the value of argument 0 to "--help".
What is wrong in the above logic or syntax?
I tried using other strings, like "help" instead of "--help" but the outcome is the same.
I'm not that familiar with expect and tcl, but I tried the expression in tclsh and the same thing happens there. So this issue has to do with invalid tcl code. The following tcsh session shows that the syntax if {$variable=="--help"} {...} is OK, but removing the white space in my string comparison attempt above does not solve the problem.
Here's the tcsh session:
% set v1 "--help"
--help
% if [v1 == "--help'] { puts "allo"}
extra characters after close-quote
% if [v1 == "--help"] { puts "allo"}
invalid command name "v1"
% if [$v1 == "--help"] { puts "allo"}
invalid command name "--help"
% if $v1 == help {puts "allo"}
invalid bareword "help"
in expression "--help";
should be "$help" or "{help}" or "help(...)" or ...
% if $v1 == "--help" {puts "allo"}
invalid bareword "help"
in expression "--help";
should be "$help" or "{help}" or "help(...)" or ...
% if {$v1=="--help"} {puts "allo"}
allo
%
The problem is this line:
puts "Usage: testit [--help] [port]"
And the problem with it is that [...] does command substitution in that situation. You need to add a couple of backslashes in there to prevent that, like this:
puts "Usage: testit \[--help] \[port]"
Or you can enclose the string in braces to inhibit all substitutions:
puts {Usage: testit [--help] [port]}
Either will work (and they'll get compiled to exactly the same thing so use whichever you prefer).

Expect - avoid sending escape prompt sequences via ssh

The script is intended to retrieve the contents of some directory when it is getting full.
For development, the 'full' was set at 15%, the directory is /var/crash.
expect "#*" {
foreach part $full {
puts "part: $part"
set dir [split $part]
puts "dir: $dir [llength $dir]"
set d [lindex $dir 0]
puts "d: $d"
send -s -- "ls -lhS $d\n"
expect "#*" { puts "for $dir :: $expect_out(buffer)"}
}
}
send "exit\r"
The output of the script is:
part: /var/crash 15%
dir: {/var/crash} 15% 2
d: /var/crash
send: sending "ls -lhS \u001b[01;31m\u001b[K/var\u001b[m\u001b[K/crash\n" to { exp7 }
expect: does "" (spawn_id exp7) match glob pattern "#*"? no
expect: does "ls -lhS \u00071;31m\u0007/var\u0007\u0007/" (spawn_id exp7) match glob pattern "#*"? no
expect: does "ls -lhS \u00071;31m\u0007/var\u0007\u0007/crash\r\n" (spawn_id exp7) match glob pattern "#*"? no
As can be seen, although $d is /var/crash, when it is sent via ssh it becomes something like \u001b[01;31m\u001b[K/var\u001b[m\u001b[K/crash.
I cannot change the remote machine definitions for the command prompt.
How to get rid of these escape sequences that are sent?
Edit: Info about $full as requested
The proc analyze just tries to filter meaningful data.
proc analyze_df {cmd txt} {
set full [list]
set lines [split $txt \n]
foreach l $lines {
if {[string match $cmd* $l]} { continue }
set lcompact [regsub -all {\s+} $l " "]
set data [split $lcompact]
if {[string match 8?% [lindex $data 4]] \
|| [string match 9?% [lindex $data 4]] \
|| [string match 1??% [lindex $data 4]] \
|| [string match 5?% [lindex $data 4]] \
|| [string match 1?% [lindex $data 4]] } {
lappend full "[lindex $data 5] [lindex $data 4]"
}
}
return $full
}
The extract about the $full that was missing.
set command0 "df -h | grep /var"
send -- "$pass\r"
expect {
-nocase "denied*" {puts "$host denied"; continue}
-nocase "Authentication failed*" {puts "$host authentication failed"; continue}
"$*" {send -s -- "$command0\n"}
timeout {puts "$host TIMEOUT"; continue}
}
expect "$*" {puts "$host -> $expect_out(buffer)" }
set full [analyze_df $command0 $expect_out(buffer)]
Taking the suggestion received, perhaps it's grep that is adding the escape sequences, no?
You don't show how $full gets its value. But it must already have the escape codes. When printing $d those escape codes are interpreted by the terminal, so they may not be obvious. But Expect/Tcl definitely doesn't insert them. This is also confirmed by the braces around the first element when you print $dir. If this element was plain /var/crash, there would be no braces.
Your remark about the command prompt would suggest that $full may be taken from there. Maybe you cannot permanently change the remote machine's command prompt, but you should be able to change it for your session by setting the PS1 environment variable.
Another trick that may help in such situations is to do set env(TERM) dumb before spawning the ssh command. If the prompt (or other tools) correctly use the tput command to generate their escape codes, a dumb terminal will result in empty strings. This won't work if the escape codes are hard-coded for one specific TERM. But that's a bug on the remote side.
If you're absolutely stuck with that input data (and can't tell things to not mangle it with those ANSI terminal colour escape codes) then you can strip them out with:
set dir [split [regsub -all {\u001b[^a-zA-z]*[a-zA-Z]} $part ""]]
This makes use of the fact that the escape sequences start with the escape character (encoded as \u001b) and continue to the first ASCII letter. Replacing them all with the empty string should de-fang them cleanly.
You are recommended to try things like altering the TERM environment variable before calling spawn so that you don't have to do such cleaning. That tends to be easier than attempting to "clean up" the data after the fact.

Read socket is blocked

I'm writing a socket utility to communicate a client to a server. When input to the socket from the client side, the server is receiving it fine. However, when input to the socket from the server, the client can't read. When checking for fblocked $channel. It is 1. I've tried everything including adding new line, ...
Please help.
Below is my code
proc read_command { sock } {
variable self
global connected
set len [gets $sock line]
set bl [fblocked $sock]
puts "Characters Read: $len Fblocked: $bl"
if {$len < 0} {
if {$bl} {
puts "Input is blocked"
} else {
set connected 1
puts "The socket was closed - closing my end"
close $sock
}
} else {
if {!$bl} {
puts "Read $len characters: $line"
catch {uplevel #0 $line} output
puts "1==>$output<=="
puts $sock "$output"
puts $sock "\n"
flush $sock
}
}
}
proc client { host port } {
variable self
set s [socket $host $port]
set self(csock) $s
set self($s,addr) $host
set self($s,port) $port
fconfigure $s -buffering line -blocking 0
return $s
}
proc prun { sock args} {
variable self
set result [list]
set cmd $args
set cmd [regsub -all {(^\s*\{)||(\}\s*$)} $cmd ""]
set cmd [string trimleft $cmd]
set o1 [eval $cmd]
#catch {uplevel #0 $cmd} o1
puts "1_$sock ==> $o1"
lappend result $o1
#--------------
puts $sock $cmd
flush $sock
set bl [fblocked $sock]
set file [read $sock]
set bl [fblocked $sock]
puts "Fblocked: $bl"
puts "Output: $file"
puts "2_$Comm::self(csock) ==> $file ==> $bl"
lappend result $file
return $result
}
Here is how I run it.
I call server on 1 of the terminal. It will echo the ip address and the port.
Then I call client using the address and the port above to get back the client socket
Then I call prun on the client shell to get back a pair of values, one with the value of the cmd call on the client, and the other the value of the cmd call on the server. Basically I would like to get the pair of values so I can use them for correlation between the 2 set of data.
Below is the code:
1.
On server shell
$ server
2.
On client shell
$ set s [client $addr $port]
3.
Call a proc to get the value from the client shell, then send the command to the server to get the value from the server shell, and return that value back to the client.
$ set res [prun $s {set val [get_attribute [get_nets mynet] pin_capacitance_max]}]
You wrote:
puts "2_$Comm::self(csock) ==> $file ==> $bl"
and defined self with variable. Are you working with packages?. May be you forgot something related to it.
For test you can use just global but using arrays would be a little more complicated.

How to search a digit i.e process id in tcl and kill the process id

I have tried to search process id i-e 6762 stored in a variable say buffer
nohup tcpdump -ni eth0 -s0 2>&1 </dev/null &
[1] 6762
You have new mail in /var/mail/root
If it matches then I want to kill it.
I have tried the following code:
foreach line [split $buffer "\n"]{
if {[regexp {\[\d\]\s+(\d+)}$line junk pid]}
break
}
if {[info exists $pid]} {
puts "PID of nohup is $pid"
}
Following error i am getting while executing the above code
wrong # args: should be "foreach varList list ?varList list ...? command"
while executing
"foreach line [split $Buffer "\n"]{"
(procedure "Test_SNMP_Trap" line 21)
invoked from within
"Test_SNMP_Trap"
(file "./SNMP_TRY.tcl" line 46)
How can i search a process id and then correctly destroy it?
Almost right. You need a space to separate the first and second arguments.
Also I would change the first \d to \d+, as there's always the possibility that you could have more than 9 background jobs.
if {[regexp {\[\d+\]\s+(\d+)} $line junk pid]}
Also [info exists ...] acts on a variable, not a value:
[info exists pid]
Edit: Add example of final code snippet
There is a missing space in the foreach line. There needs to be a space before the {. And the body of the if statement was not attached.
The parser in Tcl doesn't work in the same manner as some other languages. Line continuations and spaces are important.
So the final code will look like:
foreach line [split $buffer "\n"] {
if { [regexp {\[\d+\]\s+(\d+)} $line junk pid] } \
break
}
if { [info exists pid] } {
puts "PID of nohup is $pid"
}
The if statement could also be (better):
if { [regexp {\[\d+\]\s+(\d+)} $line junk pid] } {
break
}
If you want to try to kill it,
try {
exec sh -c "kill -0 $pid && kill $pid"
} on error e {
puts "could not kill $pid: $e"
}
The kill -0 $pid is just a test to see if such a pid is running.
It is important to put spaces and braces in in Tcl because each word to a command needs to be properly separated from all the others and end-of-line signals end-of-command unless you quote or escape it.
Thus, your code:
foreach line [split $buffer "\n"]{
if {[regexp {\[\d\]\s+(\d+)}$line junk pid]}
break
}
That has a problem in that there's no space between ] and { on the first line, a problem in that there's no space between } and $ on the second line, and a problem that there's nothing to make the third line associated with the second. Let's write it to be conventional Tcl:
foreach line [split $buffer "\n"] {
if {[regexp {\[\d\]\s+(\d+)} $line junk pid]} {
break
}
}
I've changed almost nothing; just added some spaces and some braces.
Some of the problems with the code snippet above:
missing line continuation characters
missing closing quotes
info exists should be on pid and not $pid
Try the snippet below and see if it helps:
foreach line [split $buffer "\n"] \
{
if {[regexp {\[\d\]\s+(\d+)} $line junk pid]} \
{
break
}
}
if {[info exists pid]} \
{
puts "PID of nohup is $pid"
}

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.