合并子进程stdout和stderr

时间:2016-12-07 14:19:12

标签: rust

如何合并子进程stdout和stderr?

由于无法在stdoutstderr之间共享所有权,因此以下情况无效:

let pipe = Stdio::piped();
let prog = Command::new("prog")
                        .stdout(pipe)
                        .stderr(pipe)
                        .spawn()
                        .expect("failed to execute prog");

换句话说,shell中2>&1的Rust等价物是什么?

4 个答案:

答案 0 :(得分:8)

我正在开发一个名为duct的库,这使得这很容易。它还没有完全稳定,没有记录(尽管它与Python version非常接近),但它今天有效:

#[macro_use]
extern crate duct;

fn main() {
    cmd!("echo", "hi").stderr_to_stdout().run();
}

"正确的方式"做一些这样的事情,duct在幕后为你做的是创建一个双端的OS管道,并将它的写入结束传递给stdout和stderr。标准库的Command类通常支持这类事情,因为Stdio实现了FromRawFd,但遗憾的是标准库没有公开创建管道的方法。我已在duct内写了另一个名为os_pipe的库,如果您愿意,可以直接使用它。

已在Linux,Windows和macOS上测试过。

答案 1 :(得分:2)

标准库中没有任何内容可以帮助您。并不代表你自己不能写。这也意味着您可以决定每个文件描述符的读取频率以及如何组合每个文件描述符的数据。在这里,我尝试使用默认的BufReader大小读取块,并且当两个描述符都有数据时,更喜欢将stdout数据放在第一位。

use std::io::prelude::*;
use std::io::BufReader;
use std::process::{Command, Stdio};

fn main() {
    let mut child =
        Command::new("/tmp/output")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("Couldn't run program");

    let mut output = Vec::new();

    // Should be moved to a function that accepts something implementing `Write`
    {
        let stdout = child.stdout.as_mut().expect("Wasn't stdout");
        let stderr = child.stderr.as_mut().expect("Wasn't stderr");

        let mut stdout = BufReader::new(stdout);
        let mut stderr = BufReader::new(stderr);

        loop {
            let (stdout_bytes, stderr_bytes) = match (stdout.fill_buf(), stderr.fill_buf()) {
                (Ok(stdout), Ok(stderr)) => {
                    output.write_all(stdout).expect("Couldn't write");
                    output.write_all(stderr).expect("Couldn't write");

                    (stdout.len(), stderr.len())
                }
                other => panic!("Some better error handling here... {:?}", other)
            };

            if stdout_bytes == 0 && stderr_bytes == 0 {
                // Seems less-than-ideal; should be some way of
                // telling if the child has actually exited vs just
                // not outputting anything.
                break;
            }

            stdout.consume(stdout_bytes);
            stderr.consume(stderr_bytes);
        }
    }

    let status = child.wait().expect("Waiting for child failed");
    println!("Finished with status {:?}", status);
    println!("Combined output: {:?}", std::str::from_utf8(&output))
}

最大的差距在于该过程何时退出。我对Child缺乏相关方法感到惊讶。

另见How do I prefix Command stdout with [stdout] and [sterr]?

在此解决方案中,文件描述符之间没有任何内在排序。作为类比,想象两桶水。如果你清空了一个水桶,后来看到它已经被重新填满,你知道第二个水桶是在第一个水桶之后。但是,如果您清空两个水桶并稍后返回并且两个水桶都已填满,则无法确定哪个水桶首先被填充。

"质量"交错的问题在于您从每个文件描述符中读取的频率以及首先读取的文件描述符。如果你在一个非常紧凑的循环中从每个字节读取一个字节,你可能会得到完全混乱的结果,但这些将是最准确的"关于订购。同样,如果一个节目打印" A"然后到stderr" B"到stdout但是shell在stderr之前从stdout读取,然后结果将是" BA",它向后看。

答案 2 :(得分:1)

我写了io-mux crate是为了提供一个多端的类似管道的结构;主要用例是从经过适当交错处理的进程中捕获stdout和stderr,并区分出哪个数据来自哪个数据。有关使用方法的示例,请参见highlight-stderr

(io-mux主要在Linux上运行;它也可以在其他UNIX平台上运行,但是由于这些平台上的UNIX套接字的行为而存在一些限制。)

如果您不想区分来自stdout的数据和来自stderr的数据,则可以使用普通管道。在UNIX上,使用libc::pipe创建管道,Stdio::from_raw_fd两次创建stdout和stderr,生成进程,然后从管道的另一端读取。在Windows上,您可以使用句柄而不是文件描述符来做类似的事情。

如果您不需要区分来自stdout的数据和来自stderr的数据,并且不想处理设置管道的特定于平台的细节,请尝试使用{{3} },其中特别提到了对结合stdout和stderr的支持。

答案 3 :(得分:0)

我知道您在询问 std::Command,但为了防止它对使用 subprocess 板条箱的任何人有所帮助,您可以使用 Redirection::Merge 执行此操作:

.stderr(Redirection::Merge)