I am looking for a way to create some sort of terminal "clone" in Python 3.6 using subprocess. The clone should behave just like the real terminal. The goal is to have a python script simulating a shell which behaves as much as the normal shell as possible. Including commands like cd
or variable declarations.
My target system is Linux with gnome shell, but my problems are probably cross OS relate. At first I din't think that was too hard as you can easily run terminal commands using subprocess but I encountered some problems.
What I don't want to do:
#!/usr/bin/env python
import subprocess
while True:
command = input(" >>> ").rstrip('\n')
if command == 'quit':
break
subprocess.run(command, shell=True)
There would be a very easy way to run commands one after each other. The problem with that is, that this will start a new process for every command. So if I do the following commands it doesn't work as I want to:
>>> ls
stackoverflow_subprocess.py
>>> cd ..
>>> ls
stackoverflow_subprocess.py
Because we start a new process every time, commands like cd do not have any effect. That's why I want to run all commands in the same process.
First Attempt:
#!/usr/bin/env python
from subprocess import PIPE, Popen
pipe = Popen("/bin/bash", stdin=PIPE, stdout=PIPE, stderr=PIPE)
quit_command = "quit"
while True:
command = input(" >>> ")
if command == quit_command:
break
command = str.encode(command + "\n")
out, err = pipe.communicate(command)
print(out,err)
This was my first attempt at solving my problem. This is what i got:
>>> echo hi
b'hi\n' b''
>>> echo hello
Traceback (most recent call last):
File "/home/user/Python/Stackoverflow/subprocess.py", line 11, in <module>
out, err = pipe.communicate(command)
File "/usr/lib/python3.6/subprocess.py", line 818, in communicate
raise ValueError("Cannot send input after starting communication")
ValueError: Cannot send input after starting communication
Process finished with exit code 1
So I can't just write multiple commands like this.
Second Attempt:
#!/usr/bin/env python
from subprocess import PIPE, Popen
fw = open("tmpout", "wb")
fr = open("tmpout", "r")
pipe = Popen("/bin/bash", stdin=PIPE, stdout=fw, stderr=fw, bufsize=1)
quit_command = "quit"
while True:
command = input(" >>> ")
if command == quit_command:
break
command = str.encode(command + "\n")
pipe.stdin.write(command)
out = fr.read()
print(out)
This attempt was based on another stackoverflow question which was similar to mine: Interactive input/output using python
>>> echo hi
>>> echo hello
>>> quit
Process finished with exit code 0
However, this did not work as well. out
was just an empty string. When I looked into it i realized, that the content of tmpout
does not get written to the file until the program finished. Even if you close and reopen fw
between each iteration it still just writes to tmpout
after the program finishes.
Contents of tmpout
after program finished:
hi
hello
Third Attempt:
#!/usr/bin/env python
from subprocess import PIPE, Popen
import os
import fcntl
from subprocess import Popen, PIPE
def setNonBlocking(fd):
"""
Set the file description of the given file descriptor to non-blocking.
"""
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
flags = flags | os.O_NONBLOCK
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
p = Popen("/bin/bash", stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=1)
setNonBlocking(p.stdout)
setNonBlocking(p.stderr)
quit_command = "quit"
while True:
command = input(" >>> ")
if command == quit_command:
break
command = str.encode(command + "\n")
p.stdin.write(command)
while True:
try:
out = p.stdout.read()
except IOError:
continue
else:
break
out = p.stdout.read()
print(out)
At last I tried the second solution from the Stackoverflow question mentioned above. This didn't work as well, as it just always returned None
:
>>> echo hi
None
>>> echo hello
None
>>> quit
Process finished with exit code 0
Question:
Does anyone know a way to solve any of these issues? Is it possible to communicate more commands even after communication started? Or is it possible for the file to be written before the program ends? Or does anyone know how to get the actual output instead of just None
for the last attempt?
Thank you in advance :)