PStreams
PStreams Reference

A C++ iostream interface to POSIX process I/O.

This library provides a C++ re-implementation of the POSIX.2 functions popen(3) and pclose(3), using iostreams to read from and write to the opened process.

The advantages over the standard popen() function are:

  • Standard C++ interface. PStreams classes can be used in any code that expects an iostream class, giving all the advantages of type-safety and localisation that iostreams have over the printf()/scanf() functions in standard C.
  • Bidirectional I/O. Implementations of popen() vary between systems. Some systems use bidirectional pipes, allowing reading and writing on the same stream, but this is not supported everywhere. Because PStreams doesn't use popen() but re-implements it at a lower level, bidirectional I/O is available on all systems.
  • Reading from the process' stderr. Input PStreams can read from the process' stderr as well as stdout.
  • More flexible interface. In addition to handling a shell command in the same way as popen() the PStreams classes can open a process specified by a filename and a vector of arguments, similar to the execv() function.
  • Signals. The child process can be sent any signal, which can be used to kill or otherwise control it.

The library is free software, available under the permissive Boost Software License - Version 1.0 (prior to version 1.0.1 it used the GNU LGPL).

To help improve PStreams see the SourceForge project page.

 

Current status

Version 1.0.4

Defines ipstream, opstream and pstream classes for ISO C++-compliant compilers. The classes are fully functional and the interfaces are stable.

The stream buffer class, pstreambuf, doesn't use popen(). It uses up to three pipes shared with the associated process, giving access to any combination of the process' stdin, stdout and stderr streams. I/O operations on these pipes are buffered to avoid making a system call for every character written or read.

Another class, rpstream (Restricted PStream) is similar to pstream except that the child process' stdout and stderr cannot be read directly from an rpstream. To read from the process you must call either rpstream::out() or rpstream::err() to obtain a reference to an istream that reads from the process' corresponding output stream. This class is not as well tested as the others (i.e. it's hardly tested at all).

No code-conversion is performed on multi-byte character streams. It should be possible to use the PStreams classes templatized with character types other than char (e.g. basic_pstream<int>) but note that characters are transfered in a bytewise manner, so it is the programmer's responsibility to interpret the resulting character strings. Since the classes are intended to be used to read/write data between processes, which will usually share an internal character representation, rather than to/from files, this behaviour should be sufficient.

The PStreams code has not been optimised, the emphasis is on simplicity and correctness, not performance. If you have any performance benchmarks or ideas for (portable) ways to speed up the code then please share them.

 

Usage

Please refer to the doxygen-generated API documentation, accessible through the links at the top of the page.

Using the PStreams classes is similar to using a std::fstream, except that a shell command is given rather than a filename:


// print names of all header files in current directory
redi::ipstream in("ls ./*.h");
std::string str;
while (in >> str) {
    std::cout << str << std::endl;
}

The command argument is a pointer to a null-terminated string containing a shell command line. This command is passed to /bin/sh using the -c flag; Alias and wildcard interpretation, if any, is performed by the shell.

Alternatively, the process can be started with a vector of arguments:


// remove some files, capturing any error messages
std::vector<std::string> argv;
std::vector<std::string> errors;
argv.push_back("rm");
argv.push_back("-rf");
argv.push_back("./foo.txt");
argv.push_back("./bar.html");
argv.push_back("./fnord/");
redi::ipstream in("rm", argv, redi::pstreambuf::pstderr);
std::string errmsg;
while (std::getline(in, errmsg)) {
    errors.push_back(errmsg);
}

If this form of initialisation is used and the file argument doesn't contain a slash then the actions of the shell will be duplicated in searching for an executable in PATH. The shell will not interpret the other arguments, so wildcard expansion will not take place if this interface is used.

If an rpstream was used in the example above it would be necessary to replace the while condition like so:


while (std::getline(in.err(), errmsg)) {
    errors.push_back(errmsg);
}

This form can also be used with the unrestricted pstream and ipstream classes, but it is not strictly necessary.

Here is a more complete example, showing how to use std::istream::readsome() to read without blocking:

const pstreams::pmode mode = pstreams::pstdout|pstreams::pstderr;
ipstream child("echo OUT1; sleep 1; echo ERR >&2; sleep 1; echo OUT2", mode);
char buf[1024];
std::streamsize n;
bool finished[2] = { false, false };
while (!finished[0] || !finished[1])
{
    if (!finished[0])
    {
        while ((n = child.err().readsome(buf, sizeof(buf))) > 0)
            std::cerr.write(buf, n).flush();
        if (child.eof())
        {
            finished[0] = true;
            if (!finished[1])
                child.clear();
        }
    }

    if (!finished[1])
    {
        while ((n = child.out().readsome(buf, sizeof(buf))) > 0)
            std::cout.write(buf, n).flush();
        if (child.eof())
        {
            finished[1] = true;
            if (!finished[0])
                child.clear();
        }
    }
}

Since the underlying streams controlled by the pstreambuf are pipes, seek operations will not succeed. pstreambuf::seekoff() and pstreambuf::seekpos() will always fail, even for what may seem like no-ops, such as seeking with an offset of zero relative to the current position. Because the streambuf operations aren't successful and return off_type(-1), calling std::istream::seekg() or std::ostream::seekp() on a PStream object will cause failbit to be added to the stream state. Following this you will need to call std::basic_ios::clear() on the stream object to clear the stream state information before further I/O can take place. This behaviour is analogous to calling std::fseek() on a pipe, which always fails and sets errno to ESPIPE ("Illegal seek").


The latest version of PStreams can be found at http://pstreams.sf.net