无法对生成的子进程进行多次管道传输

16
我希望能够使用Rust来生成一个子shell,然后重复传递任意命令并处理它们的输出。我在网上找到了很多示例,教我如何传递单个命令并接收其单个输出,但我似乎无法重复执行它们。
例如,下面的代码在注释后的行上挂起。(我想也许read_to_string()正在阻塞,直到它接收到子进程的标准输出,但如果是这样的话,我不明白为什么输出没有出现..)
let mut child_shell = match Command::new("/bin/bash")
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .spawn()
{
    Err(why) => panic!("couldn't spawn child_shell: {}", Error::description(&why)),
    Ok(process) => process,
};

loop {
    {
        match child_shell.stdin.as_mut().unwrap().write("ls".as_bytes()) {
            Err(why) => panic!(
                "couldn't send command to child shell: {}",
                Error::description(&why)
            ),
            Ok(_) => println!("sent command to child shell"),
        }
    }

    {
        let mut s = String::new();
        // ↓ hangs on this line ↓
        match child_shell.stdout.as_mut().unwrap().read_to_string(&mut s) {
            Err(why) => panic!("couldn't read bash stdout: {}", Error::description(&why)),
            Ok(_) => print!("bash responded with:\n{}", s),
        }
    }
}

我是一名Rust初学者,我认为问题在于我对借用检查器/引用规则的理解有限。如果从代码中删除循环指令并将对std::process::Child结构体内部的引用更改为不可变,则上述代码可以正常运行(对于单次迭代)。例如,从这个:

child_shell.stdin.as_mut().unwrap().write("ls".as_bytes())

转换为:

 child_shell.stdin.unwrap().write("ls".as_bytes())

显然,反复运行 ls 不是我的终极目标,我知道我可以编写一个 shell 脚本,然后让 Rust 反复运行它 - 但是(除了学习更多关于 Rust 的知识之外!)这是我需要能够至少在原理上完成的事情,对于一个更复杂的项目来说尤其如此(如果可能与任何解决方案相关,我很乐意进一步介绍,但这可能远远超出了这个问题的范围!)
最后,即使不能以这种方式使用子 shell,我仍然想学习如何重复/持续地将数据传输到和从运行其他任意命令的衍生进程中,因为我在 Rust 文档、教程或 Stack Overflow 中都找不到任何信息。
1个回答

19

read_to_string的文档描述为:

读取此源中的所有字节,直到EOF

因此,它将等待直到所有输入都完成,这只有在关闭shell时才会发生。您可以通过从输出中读取一组固定数量的数据来解决这个问题。这是一个例子,其中我删除了所有漂亮的错误打印,以展示解决方案的核心:

use std::process::{Command, Stdio};
use std::io::{BufRead, Write, BufReader};

fn main() {
    let mut child_shell = Command::new("/bin/bash")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let child_in = child_shell.stdin.as_mut().unwrap();
    let mut child_out = BufReader::new(child_shell.stdout.as_mut().unwrap());
    let mut line = String::new();

    loop {
        child_in.write("ls\n".as_bytes()).unwrap();
        child_out.read_line(&mut line).unwrap();
        println!("{}", line);
    }
}
在这里,我们使用BufRead特性从输入中读取内容,直到我们读取了一行。然后我们打印输出并继续循环。当然,每行输入会有多行输出,所以会有越来越多的等待被读取。
在真正的代码中,您需要确定何时停止读取。如果您有固定大小的响应,则可能非常容易,但是如果您尝试处理人机交互程序,则可能非常困难。
请注意在没有使用Option :: as_refOption :: as_mutOption :: take 的情况下直接使用child_shell.stdinstdout。直接使用stdinstdout 将使该项移出Child 结构,使Child 部分无效。例如,您将无法调用waitkill
另外,与此无关的是,您不需要像Error :: description(&why)这样调用特征方法。您可以直接使用why.description()

非常感谢!所有的建议和观察都非常有用。关于如何确定何时停止从BufReader读取,为什么在循环内尝试简单地调用child_out.lines().count()for l in child_out.lines()会给我一个“使用已移动值:child_out”的错误?为什么我可以要求BufReader提供一行,但不能要求它提供所有行?我是否在这里误解了迭代器的工作方式? - John
@John 不是迭代器的工作原理,而是这些方法中的 self 参数如何工作。查看 count 可以看到它通过值获取 self,这会消耗底层迭代器,与 lines 相同。调用 count 也没有意义,因为您必须读取 所有 进程输出才能获得计数,此时所有数据都已被丢弃。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接