通过子进程

时间:2018-09-04 10:40:38

标签: python subprocess

我正在寻找一种使用子进程在Python 3.6中创建某种终端“克隆”的方法。克隆的行为应类似于真实终端。我们的目标是要有一个python脚本来模拟一个外壳,该外壳的行为尽可能地类似于普通外壳。包括诸如cd之类的命令或变量声明。

我的目标系统是带有gnome shell的Linux,但是我的问题可能与跨OS有关。刚开始我并不觉得太难,因为您可以使用子进程轻松运行终端命令,但是遇到了一些问题。

我不想做什么:

#!/usr/bin/env python
import subprocess

while True:
    command = input(" >>> ").rstrip('\n')
    if command == 'quit':
        break
    subprocess.run(command, shell=True)

将有一种非常简单的方法来依次运行命令。这样做的问题是,这将为每个命令启动一个新进程。因此,如果我执行以下命令,它将无法正常运行:

 >>> ls
stackoverflow_subprocess.py
 >>> cd ..
 >>> ls
stackoverflow_subprocess.py

因为我们每次都启动一个新进程,所以cd之类的命令无效。这就是为什么我要在同一进程中运行所有命令。

第一次尝试:

#!/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)

这是我第一次尝试解决我的问题。这就是我得到的:

 >>> 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

所以我不能只写这样的多个命令。

第二次尝试:

#!/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)

此尝试基于另一个类似于我的Interactive input/output using python

的stackoverflow问题。
 >>> echo hi

 >>> echo hello 

 >>> quit

Process finished with exit code 0

但是,这种方法不能正常工作。 out只是一个空字符串。当我查看它时,我意识到tmpout的内容直到程序完成才被写入文件。即使您在每次迭代之间关闭并重新打开fw,它仍然只会在程序完成后写入tmpout

程序完成后tmpout 的内容:

hi
hello

第三次尝试:

#!/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)

最后,我尝试了上述Stackoverflow问题中的第二个解决方案。效果不佳,因为它总是返回None

 >>> echo hi
None
 >>> echo hello
None
 >>> quit

Process finished with exit code 0

问题: 有谁知道解决这些问题的方法?通讯开始后,是否还可以通讯更多命令?还是有可能在程序结束之前写入文件?还是有人知道如何获得实际输出,而不仅仅是最后一次尝试的None

预先感谢您:)

2 个答案:

答案 0 :(得分:0)

您只是在寻找这个吗?

#!/usr/bin/env python

import subprocess

while True:
    command = input(" >>> ").rstrip('\n')
    if command == 'quit':
        break
    subprocess.run(command, shell=True)

如下面的简短演示所示,它有一些明显的缺陷;但它肯定显示了如何仅通过单独保留子进程的stdout和stderr来摆脱困境。

 >>> echo hello
hello
 >>> foo=bar
 >>> echo "$foo"

 >>> # each command runs in a separate subshell
 >>> ^D
Traceback (most recent call last):
  File "/tmp/psh", line 6, in <module>
    command = input(" >>> ").rstrip('\n')
EOFError

答案 1 :(得分:0)

好吧,事实证明,要保持cd之类的shell内建函数或变量赋值,在同一进程中运行多个命令并非易事。但是它们可以自己在python中实现。因此,这是我使用子流程的Python交互式Shell版本:

#!/usr/bin/env python

from subprocess import run
from os import path, curdir, chdir

home_dir = path.expanduser("~")


# Gets current directory and replaces your home directory with "~"
def current_dir():
    return path.abspath(curdir).replace(home_dir, "~")


# Escapes a string by replacing spaces " " with "\s" between quotation marks
def escape_space(string):
    out = ""
    quote = False
    for letter in string:
        quote = (quote != (letter == "\""))  # quote <- quote XOR letter is "
        if quote and letter == " ":
            letter = "\s"
        out += letter
    return out


# Dictionary that holds all variables
var_dict = {}


# Handles Variables
def handle_vars(command_args):
    for i in range(len(command_args)):
        arg = command_args[i]

        # Replace variables with their value
        if arg[0] == "$":
            if arg[1:] in var_dict:
                command_args[i] = var_dict[arg[1:]]
            else:
                command_args[i] = ""

        # Add new variable
        elif "=" in arg:
            arg_split = arg.split("=")
            var_dict[arg_split[0]] = arg_split[1]


quit_flag = False

if __name__ == "__main__":
    while True:
        display_dir = "\033[34m{}\033[39m$ ".format(current_dir())  # The current directory with color
        commands = input(display_dir).rstrip('\n').split(";")

        # Repeat for all commands (multiple commands are possible with ";")
        for cmd in commands:

            cmd = escape_space(cmd)
            command_args = cmd.split(" ")

            handle_vars(command_args)

            if command_args[0] == "quit":
                quit_flag = True
                break
            elif command_args[0] == "cd":
                chdir(command_args[1])  # change execution dir
            else:
                command = " ".join(command_args).replace("\s", " ")
                run(command, shell=True)

        if quit_flag:
            break

    print("Shell Terminated.")

这使我可以使用cd,变量以及双引号(“)括起来的字符串。 希望这对遇到类似问题的人有所帮助。