19

I've been playing around with Python's subprocess module and I wanted to do an "interactive session" with bash from python. I want to be able to read bash output/write commands from Python just like I do on a terminal emulator. I guess a code example explains it better:

>>> proc = subprocess.Popen(['/bin/bash'])
>>> proc.communicate()
('user@machine:~/','')
>>> proc.communicate('ls\n')
('file1 file2 file3','')

(obviously, it doesn't work that way.) Is something like this possible, and how?

Thanks a lot

justinas
  • 6,287
  • 3
  • 26
  • 36

5 Answers5

14

Try with this example:

import subprocess

proc = subprocess.Popen(['/bin/bash'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
stdout = proc.communicate('ls -lash')

print stdout

You have to read more about stdin, stdout and stderr. This looks like good lecture: http://www.doughellmann.com/PyMOTW/subprocess/

EDIT:

Another example:

>>> process = subprocess.Popen(['/bin/bash'], shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
>>> process.stdin.write('echo it works!\n')
>>> process.stdout.readline()
'it works!\n'
>>> process.stdin.write('date\n')
>>> process.stdout.readline()
'wto, 13 mar 2012, 17:25:35 CET\n'
>>> 
Adam
  • 2,254
  • 3
  • 24
  • 42
  • 3
    The first .communicate() call works well, but if I try to communicate again, this happens: `ValueError: I/O operation on closed file`. Is there any way to keep it running? – justinas Mar 13 '12 at 14:07
  • 1
    1- the first code example can be written as `stdout = subprocess.check_output(['ls', '-lash'])`. To run a `bash` command, you could `check_output("some && command $(< file)", shell=True, executable='/bin/bash')` 2- the second code example is very fragile -- if the input/output are not in sync.; deadlock may happen. 3- if stdin/stdout are not a tty; the program may change their output. See [Q: Why not just use a pipe (popen())?](http://pexpect.readthedocs.org/en/stable/FAQ.html#whynotpipe) – jfs Feb 04 '16 at 14:27
  • 3
    Your "another example" doesn't work in Python3.5. Any way to solve it? – waitingkuo Jul 09 '16 at 18:10
  • 3
    @waitingkuo that is because in Python 3 `buffsize` defaults to -1 and in 2 it is 0, so set it to 0 in Popen and it should work. For me it did. – UpmostScarab May 22 '17 at 07:01
  • You example is not a interactive shell, is a static instruction execution. – e-info128 Sep 26 '20 at 04:51
4

Use this example in my other answer: https://stackoverflow.com/a/43012138/3555925

You can get more details in that answer.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import select
import termios
import tty
import pty
from subprocess import Popen

command = 'bash'
# command = 'docker run -it --rm centos /bin/bash'.split()

# save original tty setting then set it to raw mode
old_tty = termios.tcgetattr(sys.stdin)
tty.setraw(sys.stdin.fileno())

# open pseudo-terminal to interact with subprocess
master_fd, slave_fd = pty.openpty()

# use os.setsid() make it run in a new process group, or bash job control will not be enabled
p = Popen(command,
          preexec_fn=os.setsid,
          stdin=slave_fd,
          stdout=slave_fd,
          stderr=slave_fd,
          universal_newlines=True)

while p.poll() is None:
    r, w, e = select.select([sys.stdin, master_fd], [], [])
    if sys.stdin in r:
        d = os.read(sys.stdin.fileno(), 10240)
        os.write(master_fd, d)
    elif master_fd in r:
        o = os.read(master_fd, 10240)
        if o:
            os.write(sys.stdout.fileno(), o)

# restore tty settings back
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
Community
  • 1
  • 1
Paco
  • 411
  • 3
  • 9
4

This should be what you want

    import subprocess
    import threading
    
    p = subprocess.Popen(["bash"], stderr=subprocess.PIPE,shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    exit = False
    
    def read_stdout():
        while not exit:
            msg = p.stdout.readline()
            print("stdout: ", msg.decode())
    def read_stderro():
        while not exit:
            msg = p.stderr.readline()
            print("stderr: ", msg.decode())
    
    threading.Thread(target=read_stdout).start()
    threading.Thread(target=read_stderro).start()
    
    while not exit:
        res = input(">")
        p.stdin.write((res + '\n').encode())
        p.stdin.flush()

Test result:

>ls
>stdout:  1.py

stdout:  2.py

ssss
>stderr:  bash: line 2: ssss: command not found
sim
  • 73
  • 5
3

I wrote a module to facilitate the interaction between *nix shell and python.

def execute(cmd):
if not _DEBUG_MODE:
    ## Use bash; the default is sh
    print 'Output of command ' + cmd + ' :'
    subprocess.call(cmd, shell=True, executable='/bin/bash')
    print ''
else:
    print 'The command is ' + cmd
    print ''

Check out the whole stuff at github: https://github.com/jerryzhujian9/ez.py/blob/master/ez/easyshell.py

Jerry T
  • 1,541
  • 1
  • 19
  • 17
3

An interactive bash process expects to be interacting with a tty. To create a pseudo-terminal, use os.openpty(). This will return a slave_fd file descriptor that you can use to open files for stdin, stdout, and stderr. You can then write to and read from master_fd to interact with your process. Note that if you're doing even mildly complex interaction, you'll also want to use the select module to make sure that you don't deadlock.

amcnabb
  • 2,161
  • 1
  • 16
  • 24