1

I have a Python program, which, under certain conditions, should prompt the user for a filename. However, there is a default filename which I want to provide, which the user can edit if they wish. This means typically that they need to hit the backspace key to delete the current filename and replace it with the one they prefer.

To do this, I've adapted this answer for Python 3, into:

def rlinput(prompt, prefill=''):
    readline.set_startup_hook(lambda: readline.insert_text(prefill))
    try:
        return input(prompt)
    finally:
        readline.set_startup_hook()

new_filename = rlinput("What filename do you want?", "foo.txt")

This works as expected when the program is run interactively as intended - after backspacing and entering a new filename, new_filename contains bar.txt or whatever filename the user enters.

However, I also want to test the program using unit tests. Generally, to do this, I run the program as a subprocess, so that I can feed it input to stdin (and hence test it as a user would use it). I have some unit testing code which (simplified) looks like this:

p = Popen(['mypythonutility', 'some', 'arguments'], stdin=PIPE)
p.communicate('\b\b\bbar.txt')

My intention is that this should simulate the user 'backspacing' over the provided foo.txt, and entering bar.txt instead.

However, this doesn't seem to have the desired effect. Instead, it would appear, after some debugging, that new_filename in my program ends up with the equivalent of \b\b\bbar.txt in it. I was expecting just bar.txt.

What am I doing wrong?

Community
  • 1
  • 1
Andrew Ferrier
  • 16,664
  • 13
  • 47
  • 76

1 Answers1

3

The appropriate way to control an interactive child process from Python is to use the pexpect module. This module makes the child process believe that it is running in an interactive terminal session, and lets the parent process determine exactly which keystrokes are sent to the child process.

Pexpect is a pure Python module for spawning child applications; controlling them; and responding to expected patterns in their output. Pexpect works like Don Libes’ Expect. Pexpect allows your script to spawn a child application and control it as if a human were typing commands.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
  • OK, thanks. That makes a lot of sense (can see the parallels with expect, the Unix utility). Will take a look. Do you know why subprocess doesn't work the way I expect, though? Still confused by that. – Andrew Ferrier May 14 '15 at 00:36
  • @AndrewFerrier: It might be that the readline library in the child process discovers that it is getting input from a pipe, instead of from an interactive tty, and disables functionality such as `\b` processing. – Greg Hewgill May 14 '15 at 00:38
  • Yeah, that sounds plausible. I could hack round that in my utility, but obviously I'm then changing the program to handle the test - hardly a good idea. Will investigate Pexpect. – Andrew Ferrier May 14 '15 at 00:41
  • @AndrewFerrier: you could reimplement the corresponding parts of `pexpect` on top of `subprocess` module using `pty` module: here's [code example that shows how to read subprocess' output using `pty`](http://stackoverflow.com/a/12471855/4279) – jfs May 15 '15 at 06:02
  • After a lot of fiddling with pexpect, I eventually got it to do the right thing. The key was using `child.sendcontrol("H")` to send backspaces, which was kind of fiddly. But it all works. – Andrew Ferrier May 25 '15 at 16:16