In an ideal world, I could send any command directly to the terminal, like if Python was typing them into the terminal, rather than spawning a subprocess.
That makes sense only if by "terminal" you mean the shell from which your Python script was launched. But that shell will be waiting for Python to terminate before processing any more input, so the script cannot feed it commands to process. Or even if you started the script in the background, it still has no access to inject data that the parent shell would read as input.
But you can launch a new shell via subprocess.Popen()
, send multiple commands to it over an extended period, and let it produce output to the standard output and standard error streams inherited from the parent shell. It would look something like this:
import subprocess
# Launch a subshell
shell = subprocess.Popen(['/bin/bash'], stdin=subprocess.PIPE)
# Send commands
shell.stdin.write(b'cd ~/Downloads\n')
shell.stdin.flush()
shell.stdin.write(b'ls\n')
shell.stdin.flush()
# Terminate
shell.stdin.write(b'exit\n')
shell.stdin.flush()
shell.wait(timeout=seconds_to_wait)
Note that like any subprocess, the shell's standard input is fed via a binary stream, not a text stream. Note also that you need to ensure that commands are terminated with a newline, and you will want to flush, as shown, after each one to ensure that the shell receives it right away.
There are other ways to terminate the subshell (Popen.terminate()
, Popen.kill()
, ...), and you may need to be flexible to avoid unwantedly terminating the shell while some process started by it is still running.
You will also want to exercise care if you launch interactive commands in the subshell, because like that shell itself, they will receive their input from the Python script, not directly from a user.
Additionally, do note that bash differentiates between interactive and non-interactive shells, and somewhat between login and non-login shells. This affects which startup files are read, if any, the default values for some shell options, and probably some other things that escape me at the moment. The above example uses a non-interactive,* non-login shell. That is purposeful, but it may be that you would be better served by an interactive shell, or even an interactive login shell. Those and other details can be controlled by passing additional command-line options to bash.
*... in Bash's rather specific sense of "non-interactive". That does not prevent it from receiving commands from its standard input.