I've got an expect script that looks a bit like this:
set timeout 15
spawn someprocess
expect "a line"
expect "another line"
expect "some other line"
Essentially, it's waiting until these lines appear. There are no actions to be taken.
I don't want to write the following for every line that I'm looking for:
expect {
"a line" {}
timeout { exit 1 }
}
I want expect to return a non-zero status code (i.e. in $?) if it times out at any point. How do I do this?
You can setup an expect_before line that is run "in parallel" with any later expect commands to test for timeout. Just add after your spawn command
expect_before timeout { exit 1 }
If you want to error out if the spawned process exits, you can combine them as follows:
expect_before {
timeout { exit 1 }
eof { exit 1 }
}
Related
This question already has answers here:
Is it possible to make a bash shell script interact with another command line program?
(6 answers)
Closed 1 year ago.
I have a program JLinkExe which opens it's own prompt uponn execution. So I normally run it like this:
JLinkExe
and then type the commands at it's prompt that appears:
J-Link>
There are many applications with it's own prompt and I am interested in a general method that would enable me to send commands to any kind of application that has it's own prompt.
I already tried two methods. They both try to send commands in this order:
connect
Enter
Enter
Enter
Enter
erase
loadbin program.bin , 0x0
r
q
but both fail. Here is the first method:
{ echo 'connect';
echo '';
echo '';
echo '';
echo '';
echo 'erase';
echo 'loadbin program.bin , 0x0';
echo 'r';
echo 'q'; } | JLinkExe
And the second method (source):
JLinkExe <<EOF
connect
erase
loadbin program.bin , 0x0
r
q
EOF
I found these method on the internet but I don't understand why they fail. Especially the first one that worked in the past...
Can anyone propose any better / working / universally applicable method?
I think it might be because here-docs do not wait for output. Unfortunately for you I switched company, thus can't test my code below.
#! /bin/bash
expect <<-EOF
set timeout -1
spawn JLinkExe
expect "J-Link> " { send "connect\r" }
expect "J-Link> " { send "\r" }
expect "J-Link> " { send "\r" }
expect "J-Link> " { send "\r" }
expect "J-Link> " { send "\r" }
expect "J-Link> " { send "erase\r" }
expect "J-Link> " { send "loadbin program.bin , 0x0\r" }
expect "J-Link> " { send "r\r" }
expect "J-Link> " { send "q\r" }
expect eof
catch wait result
exit [lindex \$result 3]
EOF
exit $?
Except waits until J-Link> turns up and then sends the command through the connection.
If it doesn't work please notify me. I'll try to help you after the weekend :-)
EDIT:
A: Why did you wrap everything in expect 2>&1 <<-EOF and EOF?
You can add expect in the shebang, but I often use it as part of my Bash scripts. My knowledge of Bash is better.
B: Why a -EOF instead of EOF?
That's because <<-EOF allows leading tabs when you want to end the here-doc. You can indent it in functions for instance.
C: Why did you redirect stderr to stdout (2>&1)?
In your case I should've removed this. I took the code from one of my other answer about expect and tailored it to your needs.
D: What does catch wait result and exit [lindex \$result 3] do after we catch the eof?
Nice question, I had to look this one up a little myself:
lindex takes 4rd argument in \$result and exits the here-doc (0 is arg 1).
\$result is set by catch wait result.
Catch takes the output of wait and puts that into result.
Wait returns four integers:
First: pid of process that's being waited on.
Second: spawn ID.
Third: -1 for errors, 0 otherwise.
Forth: Exit status of the program as set by the OS.
Sources:
https://linux.die.net/man/1/expect
https://www.tcl.tk/man/tcl/TclCmd/catch.html
https://www.tcl.tk/man/tcl/TclCmd/lindex.html
Note that you have to escape the $ in the here-doc, otherwise Bash tries to process it. Hence \$result.
E: Why you exit with exit $?
Bash exits a script with the last known error code. Although you can leave it implicitly, I like to add it anyhow. It keeps the script more readable for beginners.
Given the very simple script script.expect
#!/usr/bin/expect
spawn bash
expect "#"
send "/bin/false; echo \"process returned with $?\"\r"
expect -exact "process returned with 0"
send -- "exit\r"
expect eof
I don't seem to be able how the script can not fail since /bin/false will cause the echo command to print process returned with 1, thus process returned with 0 can never be matched on the expect command. I expect expect script.expect to fail with return code 1 after expect -exact "process returned with 0".
#!/usr/bin/expect
spawn bash
expect "#"
send "/bin/true; echo \"process returned with $?\"\r"
expect -exact "process returned with 0" {
send -- "exit\r"
expect eof
exit 0
}
exit 1
Even if I change the logic of my "application" in order to be able to test it with a positive/logically negated flow the outcome is still unexplainable.
I worked through
How to make expect command in expect program script to wait for exact string matching
https://www.thegeekstuff.com/2010/10/expect-examples
https://unix.stackexchange.com/questions/66520/error-handling-in-expect
https://unix.stackexchange.com/questions/79310/expect-script-within-bash-exit-codes?rq=1
and have no clue why expect is behaving this way.
In your first script, the expect -exact... command is "succeeding" with a timeout. The default timeout is 10 seconds, and the default action on timeout is to do nothing. So the commands waits 10 seconds, matches timeout, and returns, so we continue with the next command.
You can explicitly match for timeout:
expect {
-exact "process returned with 0" {}
timeout { puts "timeout!"; exit 1 }
}
To avoid the wait to timeout, you can use a regexp that will match whether $? is 0 or 1 (or other numbers). If you put part of the regexp in a capture group (), you can then find it in built-in variable $expect_out(1,string):
expect -re {process returned with ([0-9]+)}
set returncode $expect_out(1,string)
puts "we got $returncode"
exit $returncode
Note, the regexp uses {} style quotes, because "" quotes dont allow you to use [] inside them.
I have an expect script, which executes remote shell script.Now the time taken to execute shell script is based on inputs. More input, more time, less input less time.How should I set my timeout value because if set timeout as -1, then it will exit only with eof, and in case script gets hanged, then we will have a hanged session.
After analyzing the output pattern of the ongoing script, I found "OK" coming for all the lines read from the input file.So, I used it as a progress indicator to reset my timeout counter.
Following is the fraction of code I wrote to resolve my problem :
send -- "bash scriptname.sh \r"
expect {
-re "OK" {
exp_continue
}
-re "Enter XYZ value:" {
send "0000\r"
}
timeout {
exit
}
eof {
exit
}
}
Is it possible to spawn an application, send commands, expect results, but also 'respawn' the application in case it crashed and continue from the last point? I tried creating procedures for spawning, but I am not able to catch user shell prompt once the application gets closed
So it sounds like you are doing something like telneting or sshing
into a shell then running an application. The application dies or
hangs so the prompt does not return so expect does not return and so
you are stuck. You will need to use timeout to detect a hang and restart
your entire process ( including the spawn). Below is the boiler plate I start with when writing
expect scripts. The critical thing to help you resolve your problem
is to realize that spawn not only sets the spawn_id but also returns
the pid of the spawned process. You can use that pid to kill the
spawned process if you get an eof and/or a timeout. My boiler plate
kills the spawn process on timeout. The timeout does not bail from the
expect loop but waits for the eof before exiting. it also accumulates
the output of expect command so on timeout you may be able to see
where it dies. The boilerplate is in a proc called run. Spawn the process
and pass the pid and the spawn id . Reuse the boiler plate to define other
procs that will be the steps. put the procs in a script with a counter between
them as shown and repeat. The other answerer is correct that the unless the app
restarts where you left off you can need to start from scratch. If it does start from
where you left off. make the steps granular enough that you know what command
to repeat. and step to start from.
BoilerPlate
proc run { pid spawn_id buf } {
upvar $buf buffer; #buffer to accumulate output of expect
set bad 0;
set done 0;
exp_internal 0; # set to one for extensive debug
log_user 0; # set to one to watch action
expect {
-i $spawn_id
-re {} {
append buffer $expect_out(buffer); # accumultate expect output
exp_continue;
}
timeout {
send_user "timeout\n"
append buffer $expect_out(buffer); # accumultate expect output
exec kill -9 $pid
set bad 1
exp_continue;
}
fullbuffer {
send_user " buffer is full\n"
append buffer $expect_out(buffer); # accumultate expect output
exp_continue;
}
eof {
send_user "Eof detected\n"
append buffer $expect_out(buffer); # accumultate expect output
set done 1 ;
}
}
set exitstatus [ exp_wait -i $spawn_id ];
catch { exp_close -i $spawn_id };
if { $bad } {
if { $done } {
throw EXP_TIMEOUT "Application timeout"
}
throw BAD_ERROR "unexpected failure "
}
return $exitstatus
}
set count 0
set attempts 0 ; # try 4 times
while { $count == 0 && $attempts < 4 } {
set buff ""
set pid [spawn -noecho ssh user#host ]
try {
run $pid $::spawn_id buff
incr count
run2 $pid $::spawn_id buff
incr count
run3 $pid $::spawn_id buff
incr count
run4 $pid $::spawn_id buff
incr count
} trap EXP_TIMEOUT { a b } {
puts "$a $b"
puts " program failed at step $count"
} on error { a b } {
puts "$a $b"
puts " program failed at step $count"
} finally {
if { $count == 4 } {
puts "success"
} else {
set count 0
incr attempts
puts "$buff"
puts "restarting\n"
}
}
}
I found that expect command just after send command match data from send command.
Let see, my.sh:
#!/bin/sh
read line
echo ok
my.exp (some code is redundant, I emulate DejaGNU testing framework...):
set passn 0
proc pass {msg} { global passn; incr passn; send_user "PASS: $msg\n" }
set failn 0
proc fail {msg} { global failn; incr failn; send_user "FAIL: $msg\n" }
proc check {} {
global passn failn;
send_user "TOTOAL: [expr $passn + $failn], PASSED: $passn, FAIL: $failn\n"
if {$failn == 0} { exit 0 } { exit 1 }
}
set timeout 1
spawn ./my.sh
send hello\n
expect {
-ex hello {
send_user "~$expect_out(0,string)~\n"
pass hello
}
default { fail hello }
}
expect {
-ex ok { pass ok }
default { fail ok }
}
check
When run expect my.exp I get:
spawn ./my.sh
hello
~hello~
PASS: hello
ok
PASS: ok
TOTOAL: 2, PASSED: 2, FAIL: 0
I don't understand why hello matched!! Please tell me. I already reread:
http://expect.sourceforge.net/FAQ.html
expect works with
pseudoterminal devices. They are
much like normal terminals: unless you disable echo (using expect's
stty command), whatever you send is also visible as your "terminal
output" (which is expect's input).
stty_init variable is used by expect to set up newly created
pseudoterminals. If you add set stty_init -echo to the beginnig of
my.exp, the test will begin to fail.