Tutorial: Remote Directory Lister

Introduction

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.

Top

Including the Expect Package and Setting Variables

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"
}
Top

Displaying Command-Line Help

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
}
Top

Parsing Command-Line Arguments

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.

Top

Spawning the Session

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"
    }
}
Top

Handling Errors

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
    }
}
Top

Sending the Request

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
    }
}
Top

Closing the Spawned Session

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