How do I collect ALL output from pexpect.spawn()

Viewed 1057

Currently the c.read() function only returns values from output of the program that I am calling and does not collect what happens during the interactive portion. (In other words: Waiting for user name and password). Since errors can happen during that section I need to collect all output after the cmd is called.

Using the run command works fine. I am trying to convert to using expect.spawn() since that looks very close to the ssh version of expect. Current the run() method works fine in that it works locally. I can't find getOutput() type function that would return the entire output.

import pexpect
class Expect( ):
    def Do( self, cmd, program: list = [], timeout: int = 20 ):
        # result = run( cmd, events=program, timeout=timeout).decode() 

        result = ''
        c = pexpect.spawn( cmd, encoding='utf-8', timeout=timeout )
        for curItem in program:
            # print( 'wait: ' + curItem[0] + ' resp: ' + curItem[1])
            c.expect( curItem[0] )
            c.sendline( curItem[1] )
            # result += c.read()
    
        result += c.read()

        print ( 'res: ' + str(result) )
        return result


cmd  = 'fmsadmin list files -s'
prog = [('username \\(.+\\):', 'yourUN\n'), ('password:', 'yourPW\n')]
res  = Expect().Do( cmd, prog )    # Returns everything
# use results to verify functionality

Results

ID File                  Clients Size   Status Enabled Extended Privileges                                                                   Encrypted 
1  FMServer_Sample.fmp12 0       905216 Normal fmapp fmxml fmphp fmwebdirect                                                                 No        

When I expect

username (yourUN):yourUN
password:
ID File                  Clients Size   Status Enabled Extended Privileges                                                                   Encrypted 
1  FMServer_Sample.fmp12 0       905216 Normal fmapp fmxml fmphp fmwebdirect                                                                 No        

Update

I added result += c.before + c.after just under c.sendline( curItem[1] ) which now returns: c.sendline( curItem[1] )

username (yourUN): yourUN

password:
ID File                  Clients Size   Status Enabled Extended Privileges                                                                   Encrypted 
1  FMServer_Sample.fmp12 0       905216 Normal fmapp fmxml fmphp fmwebdirect                                                                 No        

Unfortunately there are still two issues. 1 there are additional values such as returns in the output and if I provide an invalid password I get debugging details back without the apps returned values. I get:

before (last 100 chars): '\r\n/usr/local/bin/fmsadmin: Permission denied, please try again.\r\nusername (yourUN):'
after: <class 'pexpect.exceptions.TIMEOUT'>
match: None
match_index: None
etc

So in short, I'm looking for the full returned values from the CLI exactly as it would be returned if I did it by hand. Which is what run() does.

2 Answers

Pexpect, just like original TCL Expect, is kinda tricky to get the correct portions of session inputs and outputs. The challenge is to format your search patterns to match what it should, and only that. Getting this to work correctly, all you have to do is to parse the returned object with before, after and match attributes.

That said, Pexpect gets everything the terminal shows, including commands and outputs. Fortunately, passwords are not shown so, unfortunately, Pexpect can't see them.

from pexpect import spawn, EOF  # Better import only needed methods
process = spawn('bash', encoding='utf-8')
process.expect('$')             # Expects terminal prompt

process.sendline('read -p "username: " var')
process.expect('\nusername: ')  # Gets command, newline and username prompt
process.sendline('me')
process.expect('[^\n]*\n.*$')   # Gets typed string, newlind and terminal prompt
print(process.match.group(0))   # Prints whole string matched
me                              # User typed
me@myhost ~ $                   # Terminal prompt

process.sendline('read -sp "password: " var')   # Note the '-s' for 'silent'
process.expect('\npassword: ')
process.sendline('my_passwd')
process.expect('.*$')           # Trying to match a newline just doesn't work
print(process.match.group(0))
me@myhost ~ $                   # Only terminal prompt

process.sendline('exit')
process.expect(EOF)

So what you should know is what kind of problem you want to solve. Why do you want to collect absolutely everything sent and returned, including the password?

BTW 1, you might already know the issue with your plain password in the code. My recommendation: getpass:

from getpass import getpass
password = getpass()
<Then use the password>

BTW 2, just in case, have the docs around.

OK, I figured it out. Not quite expected but it makes sense. Several changes:

  1. Add result += c.before + c.after at end of for loop. This captures output from from the account and password and any error from the CLI application.
  2. Put the spawn, expect, send inside a try accept block
  3. Move result += c.read() inside Try before Except (quiet the exception)
  4. Finally call result += c.buffer during the exception.

I'll probably remove the local section in next version (once I figure out remote case)

from pexpect import *

class Expect( ):
    def Do( self, cmd, program: list = [], timeout: int = 20 ):
        local   = False
        result  = ''
        if local:
            result = runu( cmd, events=program, timeout=timeout ) 
        else:
            c = ''
            try:
                c = spawnu( cmd, timeout=timeout )
                for curItem in program:
                    c.expect( curItem[0] )
                    c.sendline( curItem[1] )
                    result += c.before + c.after
                result += c.read()
            except:
                result += c.buffer
        return result

cmd  = 'fmsadmin list files -s'
prog = [('username \\(.+\\):', 'yourUN\n'), ('password:', 'yourPW\n')]
res  = Expect().Do( cmd, prog, 5 )
print ( '-' * 50 + '\n' + str(res))
Related