Expect - avoid sending escape prompt sequences via ssh - tcl

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.

Related

Is there a TCL cmd to grep a particular line and awk a particular word

Lets say below is something i've stored in a config variable in tcl.
#set config "configuration {
test_config {
address 1.2.3.4:https
}
}"
Using tcl cmd, either grep or awk or string cmds, how do i take out "1.2.3.4:https" into a variable.
So when i use do something like below,
#puts $output
1.2.3.4:https
I know how it can be done in simple bash though
#echo $config | grep address | awk '{print $2}'
#1.2.3.4:https
Can someone please help in tcl. I tried to explore string functions, but they are not giving my required output and I'm learning about regexp now.
You're probably looking for the regexp command, especially with the -line option. The trick here is to use \s* (or \s+) to match spaces and \S+ to match non-spaces.
if {[regexp -line {^\s*address\s+(\S+)} $config -> address]} {
puts "The address is $address"
}
If there are multiple address lines, you might instead do:
foreach {-> address} [regexp -inline -all -line {^\s*address\s+(\S+)} $config] {
puts "The address is $address"
}
I'm sure Donal's suggestion is the more efficient: here's a procedural way to do it:
foreach line [split $config \n] {
lassign [regexp -inline -all {\S+} $line] first second
if {$first eq "address"} {
set output $second
break
}
}

reading file with "[" and manipulation each line TCL

I have file with the below lines (file.list):
insert_buffer [get_ports { port }] BUFF1 -new_net net -new_cell cell
I'm reading the file with the below script (read.tcl):
#! /usr/local/bin/tclsh
foreach arg $argv {
set file [open $arg r]
set data [ read $file ]
foreach line [ split $data "\n" ] {
puts $line
set name [lindex $line [expr [lsearch -all $line "-new_cell"]+1]]
puts $name
}
close $file
}
while running the above script (read.tcl file.list) I get error since I have "[" in file.list and script think its a beginning of TCL command.
list element in braces followed by "]" instead of space
while executing
"lsearch -all $line "-new_cell""
("foreach" body line 5)
invoked from within
"foreach line [ split $data "\n" ] {
How can I read the file correctly and overcome the "[" symbol?
How can I read the file correctly and overcome the "[" symbol?
I don't really understand why you are doing what you are doing (processing one Tcl script by another), but you have to make sure that each line is a valid Tcl list before submitting it to lsearch.
lsearch -all [split $line] "-new_cell"
Only split will turn an arbitrary string (containing characters special to Tcl) into a valid Tcl list.
This is one of the few times in Tcl that you need to worry about what type of data you have. $line holds a string. Don't use list commands on strings because there's no guarantee that an arbitrary string is a well-formed list.
Do this:
set fields [split $line]
# don't use "-all" here: you want a single index, not a list of indices.
set idx [lsearch -exact $fields "-new_cell"]
if {$idx == -1} {
do something here if there's no -new_cell in the line
} else {
set name [lindex $fields $idx+1]
}
In order to apply a list operation on the variable, it has to be a valid list. The variable $line is not a valid list.
It is better to use regexp rather than lsearch
regexp -- {-new_cell\s+(\S+)} $x match value
puts $value
Output :
cell

Compilation Error when running Echo Service example from Tcl Book

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.

Expect : error can't read "ip": no such variable

I am a newbie in expect / TCL and trying to parse an HTML page that has output some thing like below:
<li><p>Timestamp: Wed, 14 Nov 2012 16:37:50 -0800
<li><p>Your IP address: 202.76.243.10</p></li>
<li><p class="XXX_no_wrap_overflow_hidden">Requested URL: /</p></li>
<li><p>Error reference number: 1003</p></li>
<li><p>Server ID: FL_23F7</p></li>
<li><p>Process ID: PID_1352939870.809-1-428432242</p></li>
<li><p>User-Agent: </p></li>
My script is below. I am able to get the web page which I am not able to parse the line "Your IP address:" which is giving me errors:
#!/usr/bin/expect -f
set timeout -1
spawn telnet www.whatismyip.com 80
send "GET /\r\n"
expect
set output $expect_out(buffer)
foreach line [split $output \n] {
regexp {.*<li><p>Your IP Address Is:.*?(\d+\.\d+\.\d+\.\d+)} $line ip
if {[string length ${ip}]} {
puts $ip
}
}
The error is:
Connection closed by foreign host.
can't read "ip": no such variable
while executing
"string length ${ip}"
("foreach" body line 3)
invoked from within
"foreach line [split $output \n] {
regexp {.*<li><p>Your IP Address Is:.*?(\d+\.\d+\.\d+\.\d+)} $line ip
if {[string length ${ip}]} {
..."
(file "./t4" line 7)
Any pointers where I am doing wrong?
The regular expression did not match, so the variable was not assigned. You should check the result of regexp to see if the match succeeded; when not using the -all option to regexp, you can treat it like a boolean. Try this:
foreach line [split $output \n] {
if {[regexp {<li><p>Your IP Address Is:.*?(\d+\.\d+\.\d+\.\d+)(?!\d)} $line -> ip]} {
puts $ip
}
}
The -> is really a (weird!) variable name which will hold the whole matched string; we're not interested in it (just the parenthetical part) so we use the non-alphabetic to mnemonically say “this is going to there” (the submatch to the ip variable).
Your line contains "address" (lowercase) but you're trying to match "Address" (uppercase). Add the
-nocase option to the regexp command. Also, Tcl regular expressions cannot have mixed greediness -- the first quantifier determines if the whole expression is greedy or non-greedy (I can't find where this is documented right now).
regexp -nocase {IP Address.*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})} $line -> ip
If your ultimate goal is to get your host's external IP, then go with an API solution, such as one from exip.org:
#!/usr/bin/env tclsh
set api http://api-nyc01.exip.org/?call=ip
if {[catch {exec curl --silent $api} output]} {
puts "Failed to acquire external IP"
} else {
puts "My external IP is $output"
}
Please visit their API site for more information, especially if you live outside the USA. This solution requires curl, which you might need to install.

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.