This tutorial shows how the Expect package can be used within a Tcl command-line script to log on to a remote machine via Telnet or SSH and return the contents of the current working directory.
Note: To run this script, you must have ActiveState Expect for Windows and ActiveTcl 8.4 or later installed on your machine. You also require remote access to a Unix machine that is running Telnet or SSH.
First, run the Tcl script "remotels.tcl" to see how it operates. Then progress through the rest of the tutorial, which examines essential portions of the code and explains how Expect is incorporated.
The command-line syntax required to run "remotels.tcl" varies depending on whether you are accessing the remote machine via Telnet or SSH.
For example, if you are using Telnet (the default
-login
option), and are running "remotels.tcl" from
its default location, enter the following at the command
prompt.
C:\Tcl\bin\remotels.tcl -host hostname -user username
If, instead, you are accessing the remote host via SSH, you must
also specify "ssh" as the login type using the -login
option.
In both cases, the program prompts for a password.
When the script runs, it returns the number of files and directories in the current working directory, along with a list of their names.
For a demonstration of a similar, GUI-based script, see "tkremotels.tcl" in the Demos section of the ActiveState Expect for Windows User Guide.
The package require Expect statement makes the Expect package available to the "remotels.tcl" application. It should appear along with other necessary package commands near the beginning of any Tcl script that uses Expect. Since a version is not specified, the highest available version of Expect is loaded.
A number of variables are set, including exp::winnt_debug, which Expect uses to enable viewing of a controlled console. If this variable is not set, the console remains hidden.
The array
set command initializes the variables that are passed through
the script so that the local machine can access a directory on a
remote machine. Note that the ls variable is defined as
"bin/ls/ -A1". This runs Unix's ls
command on the
remote machine and specifies how the information is to be
displayed.
package require Expect exp_log_user 0 set exp::nt_debug 1 set timeout 10 set env(TERM) dumb array set OPTS { host "" user "" passwd "" login telnet prompt "(%|#|>|\\$) $" ls "/bin/ls -A1" }
The proc usage
procedure establishes the
command-line options for "remotels.tcl". If the script is run
without any options, a list of options and their definitions are
displayed on the command line.
proc usage {code} { global OPTS puts [expr {$code ? "stderr" : "stdout"}] \ "$::argv0 -user username -host hostname ?options? -passwd password (you will be prompted if none is given) -login type (telnet, ssh, rlogin, slogin {$OPTS(login)}) -prompt prompt (RE of prompt to expect on host {$OPTS(prompt)}) -log bool (display expect log info {[exp_log_user]}) -ls lspath (path to ls on host {$OPTS(ls)}) -help (print out this message)" exit $code }
The first part of this section uses proc parseargs
to specify which patterns must be exactly matched for the
command-line arguments to be parsed. Another significant code block
in this section involves the stty -echo
command. This
command disables echoing. When a user types a password at the
command prompt, the terminal mode is altered so that typed
characters are hidden. Once the password is read with the gets
command, the stty echo
command re-enables echoing.
proc parseargs {argc argv} { global OPTS foreach {key val} $argv { switch -exact -- $key { "-user" { set OPTS(user) $val } "-host" { set OPTS(host) $val } "-passwd" { set OPTS(passwd) $val } "-login" { set OPTS(login) $val } "-prompt" { set OPTS(prompt) $val } "-ls" { set OPTS(ls) $val } "-log" { exp_log_user $val } "-help" { usage 0 } } } } parseargs $argc $argv if {$OPTS(host) == "" || $OPTS(user) == ""} { usage 1 } if {$OPTS(passwd) == ""} { stty -echo; puts -nonewline "password required for $OPTS(host): " flush stdout gets stdin ::OPTS(passwd) stty echo } proc timedout {{msg {none}}} { send_user "Timed out (reason: $msg)\n" if {[info exists ::expect_out]} { parray ::expect_out } exit 1 }
Note: Although the stty
and
send_user
commands shown above are Expect commands,
they do not include the exp
prefix. This is to
demonstrate that, while it is considered best practice to use the
prefix, commands without the prefix are equally valid. The
exception is when you are using another Tcl extension (such as Tk)
that includes a command with the same name. If a collision occurs,
the Expect command is likely to be overridden by the command in
another extension.
If you are logging on to the remote machine using "ssh",
"slogin" or "rlogin", the information gets processed in a slightly
different manner. With any of these methods, it is necessary to
include an additional -l
option to specify a
username.
Next, the $spawn_id variable is captured, storing information about this spawn session in memory for future reference.
If you are logging in via Telnet, the final code block in this section is required to pass the username to Telnet. If the login is completed before the script times out, the exp_send command passes the username.
switch -exact $OPTS(login) { "telnet" { set pid [spawn telnet $OPTS(host)] } "ssh" - "slogin" - "rlogin" { set pid [spawn $OPTS(login) $OPTS(host) -l $OPTS(user)] } } set id $spawn_id if {$OPTS(login) == "telnet"} { expect -i $id timeout { timedout "in user login" } eof { timedout "spawn failed with eof on login" } -re "(login|Username):.*" { exp_send -i $id -- "$OPTS(user)\r" } }
The error-handling section of the script is a while
loop that anticipates a number of problems that could occur during
login. This section is not exhaustive. For example, you could also
add provisions for invalid usernames and passwords.
If the login is not completed during the allotted 10-second time
frame, which is set at the beginning of "remotels.tcl" (set
timeout 10
) and specified with expect -i $id
timeout
, the program displays an appropriate error
message.
The remainder of this loop makes use of the exp_send command to allow for other scenarios, such as the user typing "yes" when prompted to proceed with the connection, entering a password, or resetting the terminal mode.
set logged_in 0 while {!$logged_in} { expect -i $id timeout { timedout "in while loop" break } eof { timedout "spawn failed with eof" break } "Are you sure you want to continue connecting (yes/no)? " { exp_send -i $id -- "yes\r" } "\[Pp\]assword*" { exp_send -i $id -- "$OPTS(passwd)\r" } "TERM = (*) " { exp_send -i $id -- "$env(TERM)\r" } -re $OPTS(prompt) { set logged_in 1 } }
If the login is successful, the code in the if
statement below is used to send the "ls" request to display files
and directories. After the request is sent with exp_send, the resulting output is
captured in the dir
variable, which is set on the
fourth line of the code shown below.
if {$logged_in} { exp_send -i $id -- "$OPTS(ls)\r" expect -i $id timeout {timedout "on prompt"} -re $OPTS(prompt) { set dir $expect_out(buffer) } exp_send -i $id -- "exit\r" if {[info exists dir]} { regsub "\r" $dir "" dir set files [split $dir \n] set files [lrange $files 1 [expr {[llength $files]-2}]] puts "\n[llength $files] FILES AND DIRS:" puts $files } }
The exp_close
command ends the session spawned by "remotels.tcl". Just to be sure
that session does indeed close, the exp_wait command causes the script to
continue running until a result is obtained from the system
processes. If the system hangs, it is likely because
exp_close
was not able to close the spawned process,
and you may need to kill it manually.
exp_close -i $id exp_wait -i $id