运行命令并像在终端

时间:2015-08-10 18:20:18

标签: python subprocess tty pexpect pty

我试图在Python中找到一种方法来运行其他程序:

  1. 可以记录正在运行的程序的stdout和stderr 分开。
  2. 正在运行的程序的stdout和stderr可以 近实时查看,如果子进程挂起,那么 用户可以看到。 (即我们不等待执行完成之前 将stdout / stderr打印给用户)
  3. 奖金标准: 正在运行的程序不知道它是通过python运行的,因此 不会做意想不到的事情(比如chunk它的输出而不是 实时打印,或退出,因为它需要终端 查看其输出)。这个小标准几乎意味着我们需要 我认为使用pty。
  4. 这是我到目前为止所得到的...... 方法1:

    def method1(command):
        ## subprocess.communicate() will give us the stdout and stderr sepurately, 
        ## but we will have to wait until the end of command execution to print anything.
        ## This means if the child process hangs, we will never know....
        proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable='/bin/bash')
        stdout, stderr = proc.communicate() # record both, but no way to print stdout/stderr in real-time
        print ' ######### REAL-TIME ######### '
        ########         Not Possible
        print ' ########## RESULTS ########## '
        print 'STDOUT:'
        print stdout
        print 'STDOUT:'
        print stderr
    

    方法2

    def method2(command):
        ## Using pexpect to run our command in a pty, we can see the child's stdout in real-time,
        ## however we cannot see the stderr from "curl google.com", presumably because it is not connected to a pty?
        ## Furthermore, I do not know how to log it beyond writing out to a file (p.logfile). I need the stdout and stderr
        ## as strings, not files on disk! On the upside, pexpect would give alot of extra functionality (if it worked!)
        proc = pexpect.spawn('/bin/bash', ['-c', command])
        print ' ######### REAL-TIME ######### '
        proc.interact()
        print ' ########## RESULTS ########## '
        ########         Not Possible
    

    方法3:

    def method3(command):
        ## This method is very much like method1, and would work exactly as desired
        ## if only proc.xxx.read(1) wouldn't block waiting for something. Which it does. So this is useless.
        proc=subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, executable='/bin/bash')
        print ' ######### REAL-TIME ######### '
        out,err,outbuf,errbuf = '','','',''
        firstToSpeak = None
        while proc.poll() == None:
                stdout = proc.stdout.read(1) # blocks
                stderr = proc.stderr.read(1) # also blocks
                if firstToSpeak == None:
                    if stdout != '': firstToSpeak = 'stdout'; outbuf,errbuf = stdout,stderr
                    elif stderr != '': firstToSpeak = 'stderr'; outbuf,errbuf = stdout,stderr
                else:
                    if (stdout != '') or (stderr != ''): outbuf += stdout; errbuf += stderr
                    else:
                        out += outbuf; err += errbuf;
                        if firstToSpeak == 'stdout': sys.stdout.write(outbuf+errbuf);sys.stdout.flush()
                        else: sys.stdout.write(errbuf+outbuf);sys.stdout.flush()
                        firstToSpeak = None
        print ''
        print ' ########## RESULTS ########## '
        print 'STDOUT:'
        print out
        print 'STDERR:'
        print err
    

    要尝试使用这些方法,您需要import sys,subprocess,pexpect

    pexpect是纯python,可以和

    一起使用
      

    sudo pip install pexpect

    我认为解决方案将涉及python的pty模块 - 这是一种黑色艺术,我找不到任何知道如何使用的人。也许SO知道:) 作为单挑,我建议您使用www.google.com' curl。作为测试命令,因为它出于某种原因在stderr上打印出状态:D

    UPDATE-1:
    好的,所以pty库不适合人类消费。本质上,文档是源代码。 任何提出的阻塞而非异步的解决方案都无法在此处运行。 Padraic Cunningham的Threads / Queue方法效果很好,虽然不可能添加pty支持 - 而且它很脏' (引用Freenode' s #python)。 似乎唯一适合生产标准代码的解决方案是使用Twisted框架,它甚至支持pty作为布尔开关来运行进程,就像从shell调用它们一样。 但是将Twisted添加到项目中需要完全重写所有代码。这总是无聊:/

    UPDATE-2:

      

    提供了两个答案,其中一个答案解决了前两个问题   标准,并将在你需要stdout和   stderr使用Threads and Queue。另一个答案使用select,a   用于读取文件描述符的非阻塞方法,以及用于读取文件描述符的方法   "特技"产生的过程让人相信它在真实中运行   终端就好像是直接从Bash运行 - 但可能会也可能不会   有副作用。我希望我能接受这两个答案,因为   "正确"方法真的取决于情况和你为什么   首先是子处理,但是,我只能接受一个。

3 个答案:

答案 0 :(得分:17)

  

正在运行的程序的stdout和stderr可以单独记录。

您无法使用pexpect,因为stdout和stderr会转到相同的pty,之后无法将它们分开。

  

可以近乎实时地查看正在运行的程序的stdout和stderr,这样如果子进程挂起,用户就可以看到。 (即我们不会在将stdout / stderr打印给用户之前等待执行完成)

如果子流程的输出不是tty,那么it is likely that it uses a block buffering因此如果它没有产生大量输出,那么它将不会是"实时&#34 ; 例如,如果缓冲区是4K,那么您的父Python进程将无法看到任何内容,直到子进程打印4K字符并且缓冲区溢出或显式刷新(在子进程内)。此缓冲区位于子进程内部,没有标准方法可以从外部进行管理。这是显示stdio缓冲区和command 1 | command2 shell管道的管道缓冲区的图片:

pipe/stdio buffers

  

正在运行的程序不知道它是通过python运行的,因此不会做意想不到的事情(比如chunk它的输出而不是实时打印,或退出因为它需要终端查看其输出)

看起来,你的意思相反,即,如果输出被重定向到管道(当你使用stdout=PIPE时,你的子进程可能会对其输出进行分块而不是尽快刷新每个输出行蟒蛇)。这意味着默认threadingasyncio solutions无法正常工作。

有几种方法可以解决它:

  • 该命令可以接受命令行参数,例如grep --line-bufferedpython -u,以禁用块缓冲。

  • stdbuf works for some programs即,您可以使用上面的线程或asyncio解决方案运行['stdbuf', '-oL', '-eL'] + command,您应该分别使用stdout,stderr并且线条应该近乎实时显示:

    #!/usr/bin/env python3
    import os
    import sys
    from select import select
    from subprocess import Popen, PIPE
    
    with Popen(['stdbuf', '-oL', '-e0', 'curl', 'www.google.com'],
               stdout=PIPE, stderr=PIPE) as p:
        readable = {
            p.stdout.fileno(): sys.stdout.buffer, # log separately
            p.stderr.fileno(): sys.stderr.buffer,
        }
        while readable:
            for fd in select(readable, [], [])[0]:
                data = os.read(fd, 1024) # read available
                if not data: # EOF
                    del readable[fd]
                else: 
                    readable[fd].write(data)
                    readable[fd].flush()
    
  • 最后,您可以使用两个pty来尝试select + pty解决方案:

    #!/usr/bin/env python3
    import errno
    import os
    import pty
    import sys
    from select import select
    from subprocess import Popen
    
    masters, slaves = zip(pty.openpty(), pty.openpty())
    with Popen([sys.executable, '-c', r'''import sys, time
    print('stdout', 1) # no explicit flush
    time.sleep(.5)
    print('stderr', 2, file=sys.stderr)
    time.sleep(.5)
    print('stdout', 3)
    time.sleep(.5)
    print('stderr', 4, file=sys.stderr)
    '''],
               stdin=slaves[0], stdout=slaves[0], stderr=slaves[1]):
        for fd in slaves:
            os.close(fd) # no input
        readable = {
            masters[0]: sys.stdout.buffer, # log separately
            masters[1]: sys.stderr.buffer,
        }
        while readable:
            for fd in select(readable, [], [])[0]:
                try:
                    data = os.read(fd, 1024) # read available
                except OSError as e:
                    if e.errno != errno.EIO:
                        raise #XXX cleanup
                    del readable[fd] # EIO means EOF on some systems
                else:
                    if not data: # EOF
                        del readable[fd]
                    else:
                        readable[fd].write(data)
                        readable[fd].flush()
    for fd in masters:
        os.close(fd)
    

    我不知道对stdout,stderr使用不同的pty s会产生什么副作用。您可以尝试在您的情况下单个pty是否足够,例如设置stderr=PIPE并使用p.stderr.fileno()而不是masters[1]Comment in sh source suggests that there are issues if stderr not in {STDOUT, pipe}

答案 1 :(得分:2)

虽然JF Sebastian的答案肯定解决了问题的核心,但我正在运行python 2.7(这不符合原始标准)所以我只是把它扔给那些只想剪切/粘贴的其他疲惫的旅行者一些代码。 我还没有彻底测试过,但是在我试过的所有命令上它似乎完美地工作了:) 你可能想将.decode('ascii')更改为.decode('utf-8') - 我仍在测试那个位。

#!/usr/bin/env python2.7
import errno
import os
import pty
import sys
from select import select
import subprocess
stdout = ''
stderr = ''
command = 'curl google.com ; sleep 5 ; echo "hey"'
masters, slaves = zip(pty.openpty(), pty.openpty())
p = subprocess.Popen(command, stdin=slaves[0], stdout=slaves[0], stderr=slaves[1], shell=True, executable='/bin/bash')
for fd in slaves: os.close(fd)

readable = { masters[0]: sys.stdout, masters[1]: sys.stderr }
try:
    print ' ######### REAL-TIME ######### '
    while readable:
        for fd in select(readable, [], [])[0]:
            try: data = os.read(fd, 1024)
            except OSError as e:
                if e.errno != errno.EIO: raise
                del readable[fd]
            finally:
                if not data: del readable[fd]
                else:
                    if fd == masters[0]: stdout += data.decode('ascii')
                    else: stderr += data.decode('ascii')
                    readable[fd].write(data)
                    readable[fd].flush()
except: pass
finally:
    p.wait()
    for fd in masters: os.close(fd)
    print ''
    print ' ########## RESULTS ########## '
    print 'STDOUT:'
    print stdout
    print 'STDERR:'
    print stderr

答案 2 :(得分:1)

如果你想从stderr和stdout中读取并单独获取输出,你可以使用带有队列的线程,而不是过度测试,但类似如下:

import threading
import queue

def run(fd, q):
    for line in iter(fd.readline, ''):
        q.put(line)
    q.put(None)


def create(fd):
    q = queue.Queue()
    t = threading.Thread(target=run, args=(fd, q))
    t.daemon = True
    t.start()
    return q, t


process = Popen(["curl","www.google.com"], stdout=PIPE, stderr=PIPE,
                universal_newlines=True)

std_q, std_out = create(process.stdout)
err_q, err_read = create(process.stderr)

while std_out.is_alive() or err_read.is_alive():
        for line in iter(std_q.get, None):
            print(line)
        for line in iter(err_q.get, None):
            print(line)