pexpect : Python’s expect module to Control Interactive Programs

We are familiar with Expect, a Tcl package written by Don Libes.  Using this Tcl package we can control arbitrary interactive programs and processes.  One of the classic examples shows how to to expect to automate a login sequence.

Did you know Python has an “Expect” like module too?  It is called pexpect, and the author wrote it based on the Tcl Expect, but he wanted an easier interface.  His solution was to develop a Python module with most of the capabilities of Expect.

Example

This example consists of two parts:

1) A parent Python script, and 2) An interactive ‘c’ program called ioTest.

The ‘c’ program reads stdin, then lowercases the characters it receives and write the resulting string to stdout.

First, we show the execution of ioTest by itself:

$ ./ioTest
ZZZ
Child lowercases sends zzz
YYY
Child lowercases sends yyy
XXX
Child lowercases sends xxx

Now we show remote control of ioTest from Python:

$ ./pexpectTest.py

Write to subprocess: 'AA'
Received from child:  aa

Write to subprocess: 'BB'
Received from child:  bb

Write to subprocess: 'CC'
Received from child:  cc

Write to subprocess: 'DD'
Received from child:  dd

Source Code

Python

import pexpect

if __name__ == '__main__':
    shell_cmd = "/home/anukul/ioTest"
    child = pexpect.spawn(shell_cmd)

    for cc in ['AA','BB','CC','DD']:
        msg = "%s" % cc
        print "\nWrite to subprocess: '%s'" % msg.strip()
        child.sendline(msg)
        n = child.expect(["[a-z]+\r\n"])
        print "Received from child: ", child.match.group(0)

 Another example

We have one “command line utility : cmlsh” to configure network interface

Fisrt, we will execute shell manually

root@verma:[ji]$ cmlsh

ZebOS-XP version 2.1.0 IPIRouter 01/09/17 00:23:55
verma>en
verma#
verma#configure terminal
Enter configuration commands, one per line. End with CNTL/Z.
verma(config)#
verma(config)#interface eth2
verma(config-if)#
verma(config-if)#mtu 1300
verma(config-if)#end
verma#
verma#show running-config interface eth2
!
interface eth2
 mtu 1300
!
verma#
verma#exit
root@verma:[ji]$

Now we will write one python script to do the same

root@verma:[ji]$ cat run_cmlsh.py
#! /usr/local/bin/python
import pexpect

if __name__ == '__main__':
 shell_cmd = "cmlsh"
 child = pexpect.spawn(shell_cmd)

 child.expect(".*>")
 child.sendline("enable")

 # Iterate over the commands
 for cmd in ['conf t','interface eth2','mtu 1380','end','show running-config interface eth2','exit']:
   msg = ("%s\r") %cmd
   child.sendline(msg)
   child.expect(".*#")
   print child.after

 child.interact()

When we run this script we can do the same thing what we did manually

root@verma:[ji]$ python run_cmlsh.py
enable
verma#conf t
Enter configuration commands, one per line. End with CNTL/Z.
verma(config)#

verma(config)#interface eth2
verma(config-if)#

verma(config-if)#mtu 1380
verma(config-if)#

verma(config-if)#end
verma#

verma#show running-config interface eth2
!
interface eth2
 mtu 1380
!
verma#

verma#

Match multiple responses

The pattern given to expect() may be a regular expression or it may also be a list of regular expressions. This allows you to match multiple optional responses. The expect() method returns the index of the pattern that was matched. For example, say you wanted to login to a server. After entering a password you could get various responses from the server – your password could be rejected; or you could be allowed in and asked for your terminal type; or you could be let right in and given a command prompt. The following code fragment gives an example of this:

child.expect('password:')
child.sendline(my_secret_password)
# We expect any of these three patterns...
i = child.expect (['Permission denied', 'Terminal type', '[#\$] '])
if i==0:
    print('Permission denied on host. Can\'t login')
    child.kill(0)
elif i==1:
    print('Login OK... need to send terminal type.')
    child.sendline('vt100')
    child.expect('[#\$] ')
elif i==2:
    print('Login OK.')
    print('Shell command prompt', child.after)

If nothing matches an expected pattern then expect() will eventually raise a TIMEOUT exception. The default time is 30 seconds, but you can change this by passing a timeout argument to expect():

# Wait no more than 2 minutes (120 seconds) for password prompt.
child.expect('password:', timeout=120)

Example log input and output to a file:

child = pexpect.spawn('some_command')
fout = open('mylog.txt','wb')
child.logfile = fout

Example log to stdout:

# In Python 2:
child = pexpect.spawn('some_command')
child.logfile = sys.stdout

# In Python 3, spawnu should be used to give str to stdout:
child = pexpect.spawnu('some_command')
child.logfile = sys.stdout

The logfile_read and logfile_send members can be used to separately log the input from the child and output sent to the child. Sometimes you don’t want to see everything you write to the child. You only want to log what the child sends back. For example:

child = pexpect.spawn('some_command')
child.logfile_read = sys.stdout

You will need to pass an encoding to spawn in the above code if you are using Python 3.

To separately log output sent to the child use logfile_send:

child.logfile_send = fout

Special EOF and TIMEOUT patterns

If the child has died and you have read all the child’s output then ordinarily expect() will raise an EOF exception. You can read everything up to the EOF without generating an exception by using the EOF pattern expect. In this case everything the child has output will be available in the beforeproperty.

If nothing matches an expected pattern then expect() will eventually raise a TIMEOUT exception. The default time is 30 seconds, but you can change this by passing a timeout argument to expect():

index = handler.expect([expect_str, pexpect.EOF, pexpect.TIMEOUT],int(TIMEOUT_SEC))
 if index == 0:
   # Do your stuffs
 elif index == 1:
   print "EOF exception.\n"
   handler.kill(0)
   sys.exit(1)
 else:
   print "TIMEOUT exception.\n"
   handler.kill(0)
   sys.exit(1)

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s