A C++17 library for spawning and managing child processes, modelled on the
Rust subprocess crate.
Popen— low-level handle to a child process. Supports pipes for stdin/stdout/stderr,wait,poll, andwait_timeout.Exec— fluent builder for configuring and launching a process with argument lists, environment variables, working-directory overrides, and I/O redirection.Result<T>— lightweight error-or-value type used throughout the API instead of exceptions.Redirection— flexible I/O routing: pipes, file descriptors, paths (read/write/append),Merge(2>&1), or inherited.ExitStatus— discriminated union ofExited,Signaled,Other, andUndetermined.
- CMake ≥ 3.15
- C++17 compiler (GCC, Clang)
- POSIX platform (Linux, macOS) — Windows support is planned
# Library only
cmake -Bbuild -DCMAKE_INSTALL_PREFIX=~/.local
cmake --build build --config Release
# Install
cmake --build build --target install --config Releasecmake -Bbuild -DSubprocess_ENABLE_UNIT_TESTING=1
cmake --build build --config Release
cd build && ctest -C Release -VVOr with the Makefile shorthand (cleans build/ first):
make testAll types live in the subprocess namespace. Link against Subprocess::Subprocess.
#include "subprocess/Popen.hpp"
auto result = subprocess::Popen::create({"echo", "hello"}, subprocess::PopenConfig{});
if (result.ok()) {
auto p = result.take_value();
auto exit = p.wait().or_throw();
assert(exit.success());
}subprocess::PopenConfig cfg;
cfg.stdout = subprocess::Redirection::Pipe();
auto p = subprocess::Popen::create({"echo", "yolo"}, cfg).or_throw();
std::string line;
std::getline(*p.std_out, line); // line == "yolo"
p.wait().or_throw();subprocess::PopenConfig cfg;
cfg.stdin = subprocess::Redirection::Pipe();
cfg.stdout = subprocess::Redirection::Pipe();
auto grep = subprocess::Popen::create({"grep", "apple"}, cfg).or_throw();
*grep.std_in << "apple\nbanana\npineapple\nlemon\n";
grep.std_in->close();
auto exit = grep.wait().or_throw();
assert(exit.success());
std::string output = grep.std_out->slurp();
// output == "apple\npineapple\n"subprocess::PopenConfig cfg;
cfg.stdin = subprocess::Redirection::Read("input.txt").or_throw();
cfg.stdout = subprocess::Redirection::Pipe();
auto p = subprocess::Popen::create({"grep", "foo"}, cfg).or_throw();
p.wait().or_throw();auto [read_fd, write_fd] = subprocess::pipe().or_throw(); // returns std::tuple<int,int>
subprocess::PopenConfig cat_cfg, grep_cfg;
cat_cfg.stdout = subprocess::Redirection::FileDescriptor(write_fd);
grep_cfg.stdin = subprocess::Redirection::FileDescriptor(read_fd);
grep_cfg.stdout = subprocess::Redirection::Pipe();
auto cat = subprocess::Popen::create({"cat", "data.txt"}, cat_cfg).or_throw();
auto grep = subprocess::Popen::create({"grep", "sp"}, grep_cfg).or_throw();
// Close the parent's copies of the pipe ends
::close(read_fd);
::close(write_fd);
cat.wait().or_throw();
grep.wait().or_throw();
std::string out = grep.std_out->slurp();auto p = subprocess::Popen::create({"/bin/sh", "-c", "exit 42"}, {}).or_throw();
auto status = p.wait().or_throw();
if (status.is_a<subprocess::ExitStatus::Exited>()) {
int code = status.get<subprocess::ExitStatus::Exited>().code; // 42
}
if (status.is_a<subprocess::ExitStatus::Signaled>()) {
int sig = status.get<subprocess::ExitStatus::Signaled>().signal;
}| Method | Description |
|---|---|
Popen::create(argv, config) |
Spawn a child process; returns Result<Popen> |
wait() |
Block until the process exits; returns Result<ExitStatus> |
poll() |
Non-blocking check; returns std::optional<ExitStatus> |
wait_timeout(ms) |
Block up to ms milliseconds; returns Result<optional<ExitStatus>> |
exit_status() |
Return cached exit status without a syscall |
pid() |
Return the child PID if still running |
std_in, std_out, std_err |
std::optional<boost::fdostream/fdistream> pipe streams |
| Field | Type | Default | Description |
|---|---|---|---|
stdin |
Redirection |
None |
Child's standard input |
stdout |
Redirection |
None |
Child's standard output |
stderr |
Redirection |
None |
Child's standard error |
detached |
bool |
false |
Don't reap child on Popen destruction |
executable |
optional<string> |
nullopt |
Override argv[0] as the executed binary |
env |
optional<vector<EnvVar>> |
nullopt |
Full environment (nullopt = inherit) |
cwd |
optional<string> |
nullopt |
Working directory (nullopt = inherit) |
setuid |
optional<uid_t> |
nullopt |
Drop to this UID before exec |
setgid |
optional<gid_t> |
nullopt |
Drop to this GID before exec |
setpgid |
bool |
false |
Call setpgid(0,0) before exec |
| Factory | Description |
|---|---|
Redirection::None() |
Inherit from parent (default) |
Redirection::Pipe() |
Create a pipe; parent end available as std_in/out/err |
Redirection::Merge() |
Merge with the other output stream (2>&1) |
Redirection::FileDescriptor(fd) |
Use an existing file descriptor |
Redirection::Read(path) |
Open a file for reading and pass it as stdin |
Redirection::Write(path) |
Open a file for writing (truncate) |
Redirection::Append(path) |
Open a file for writing (append) |
| Method | Description |
|---|---|
ok() |
Returns true if holding a value |
take_value() |
Move the success value out |
take_error() |
Move the PopenError out |
or_throw() |
Return value or throw SubprocessException |
Exec::cmd("program").arg("--flag").cwd("/tmp").set_stdout(Redirection::Pipe())
| Method | Description |
|---|---|
Exec::cmd(command) |
Construct an Exec for command |
arg(s) / add_args(v) |
Append argument(s) |
env(k, v) / env_extend(v) / env_clear() / env_remove(k) |
Manage environment |
cwd(path) |
Set working directory |
set_stdin/set_stdout/set_stderr(r) |
Configure I/O redirection |
detached() |
Mark the process as detached |
Exec::popen(), join(), capture(), stream_stdout(), stream_stdin(), and
Exec::shell() are currently being implemented (see tickets/).
The core Popen API is complete and tested. The Exec builder and Pipeline
type are under active development. See the tickets/ directory for the full
backlog.