I l@ve RuBoard Previous Section Next Section

6.7 Capturing the Output and Error Streams from a Unix Shell Command

Credit: Brent Burley

6.7.1 Problem

You need to run an external process in a Unix-like environment and capture both the output and error streams from the external process.

6.7.2 Solution

The popen2 module lets you capture both streams, but you also need help from fcntl to make the streams nonblocking and thus avoid deadlocks:

import os, popen2, fcntl, FCNTL, select

def makeNonBlocking(fd):
    fl = fcntl.fcntl(fd, FCNTL.F_GETFL)
    try:
      fcntl.fcntl(fd, FCNTL.F_SETFL, fl | FCNTL.O_NDELAY)
    except AttributeError:
      fcntl.fcntl(fd, FCNTL.F_SETFL, fl | FCNTL.FNDELAY)

def getCommandOutput(command):
    child = popen2.Popen3(command, 1) # Capture stdout and stderr from command
    child.tochild.close(  )             # don't need to write to child's stdin
    outfile = child.fromchild
    outfd = outfile.fileno(  )
    errfile = child.childerr
    errfd = errfile.fileno(  )
    makeNonBlocking(outfd)            # Don't deadlock! Make fd's nonblocking.
    makeNonBlocking(errfd)
    outdata = errdata = ''
    outeof = erreof = 0
    while 1:
        ready = select.select([outfd,errfd],[],[]) # Wait for input
        if outfd in ready[0]:
            outchunk = outfile.read(  )
            if outchunk == '': outeof = 1
            outdata = outdata + outchunk
        if errfd in ready[0]:
            errchunk = errfile.read(  )
            if errchunk == '': erreof = 1
            errdata = errdata + errchunk
        if outeof and erreof: break
        select.select([],[],[],.1) # Allow a little time for buffers to fill
    err = child.wait(  )
    if err != 0:
        raise RuntimeError, '%s failed with exit code %d\n%s' % (
            command, err, errdata)
    return outdata

def getCommandOutput2(command):
    child = os.popen(command)
    data = child.read(  )
    err = child.close(  )
    if err:
        raise RuntimeError, '%s failed with exit code %d' % (command, err)
    return data

6.7.3 Discussion

This recipe shows how to execute a Unix shell command and capture the output and error streams in Python. By contrast, os.system sends both streams directly to the terminal. The presented getCommandOutput(command) function executes a command and returns the command's output. If the command fails, an exception is raised, using the text captured from the command's stderr as part of the exception's arguments.

Most of complexity of this code is due to the difficulty of capturing both the output and error streams of the child process at the same time. Normal (blocking) read calls may deadlock if the child is trying to write to one stream, and the parent is waiting for data on the other stream, so the streams must be set to nonblocking, and select must be used to wait for data on the streams.

Note that the second select call adds a 0.1-second sleep after each read. Counterintuitively, this allows the code to run much faster, since it gives the child time to put more data in the buffer. Without this, the parent may try to read only a few bytes at a time, which can be very expensive.

If you want to capture only the output, and don't mind the error stream going to the terminal, you can use the much simpler code presented in getCommandOutput2. If you want to suppress the error stream altogether, that's easy, too. You can append 2>/dev/null to the command. For example:

ls -1 2>/dev/null

Since Version 2.0, Python includes the os.popen4 function, which combines the output and error streams of the child process. However, the streams are combined in a potentially messy way, depending on how they are buffered in the child process, so this recipe can still help.

6.7.4 See Also

Documentation of the standard library modules os, popen2, fcntl, and select in the Library Reference.

    I l@ve RuBoard Previous Section Next Section