How to capture the output of a process piped into a Rust program?
Asked Answered
W

5

16

I know how to read the command line arguments, but I am having difficulties reading the command output from a pipe.

  1. Connect a program (A) that outputs data to my Rust program using a pipe:

    A | R
    
  2. The program should consume the data line by line as they come.

    $ pwd | cargo run should print the pwd output.

    OR

    $ find . | cargo run should output the find command output which is more than 1 line.

Wale answered 9/4, 2018 at 12:30 Comment(0)
D
20

Use BufRead::lines on a locked handle to standard input:

use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let line = line.expect("Could not read line from standard in");
        println!("{}", line);
    }
}

If you wanted to reuse the allocation of the String, you could use the loop form:

use std::io::{self, Read};

fn main() {
    let stdin = io::stdin();
    let mut stdin = stdin.lock(); // locking is optional

    let mut line = String::new();

    // Could also `match` on the `Result` if you wanted to handle `Err` 
    while let Ok(n_bytes) = stdin.read_to_string(&mut line) {
        if n_bytes == 0 { break }
        println!("{}", line);
        line.clear();
    }
}
Daphnedaphnis answered 9/4, 2018 at 13:40 Comment(3)
If I understand correctly lock(), this will break the purpose of a pipe, find will write its output in a stream, but you are currently lock and read only what find has already read. A while loop with read_line() will not be more idiomatic and functional ?There
@There that is incorrect. lock means that nothing else in the process may read from STDIN, not that it will stop reading data from outside the process. This has more performance than the loop suggested elsewhere (but is not the fastest possibility) and iterators are very idiomatic.Daphnedaphnis
tools like grep doesn't lock when its invoked alone. How does that work?Grayce
S
7

You just need to read from Stdin.

This is based on an example taken from the documentation:

use std::io;

fn main() {
    loop {
        let mut input = String::new();
        match io::stdin().read_line(&mut input) {
            Ok(len) => if len == 0 {
                return;
            } else {
                println!("{}", input);
            } 
            Err(error) => {
                eprintln!("error: {}", error);
                return;
            }
        }
    }
}

It's mostly the docs example wrapped in a loop, breaking out of the loop when there is no more input, or if there is an error.

The other changes is that it's better in your context to write errors to stderr, which is why the error branch uses eprintln!, instead of println!. This macro probably wasn't available when that documentation was written.

Sacksen answered 9/4, 2018 at 13:27 Comment(2)
Thanks Peter, I did found this after posting this question, but this fails to read the output greater than 1 line.Wale
You can just do this in a loop, and bail out when the line has zero length. I'll update my answer with that.Sacksen
A
3

You can do it in a pretty snazzy and concise way with rust's iterator methods

use std::io::{self, BufRead};

fn main() {

    // get piped input
    //   eg `cat file | ./program`
    //    ( `cat file | cargo run` also works )

    let input = io::stdin().lock().lines().fold("".to_string(), |acc, line| {
        acc + &line.unwrap() + "\n"
    });

    dbg!(input);

}

Autotomy answered 28/7, 2022 at 18:32 Comment(0)
W
2
use std::io;

fn main() {
    loop {
        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("failed to read from pipe");
        input = input.trim().to_string();
        if input == "" {
            break;
        }
        println!("Pipe output: {}", input);
    }
}

OUTPUT:

[18:50:29 Abhinickz@wsl -> pipe$ pwd
/mnt/d/Abhinickz/dev_work/learn_rust/pipe
[18:50:46 Abhinickz@wsl -> pipe$ pwd | cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
    Running `target/debug/pipe`
Pipe output: /mnt/d/Abhinickz/dev_work/learn_rust/pipe
Wale answered 9/4, 2018 at 13:26 Comment(0)
M
0

The Result<String> of read_to_string() is too simple, so please change it.

use std::io::{self, IsTerminal};

fn main() {
    let stdin = io::stdin();
    if !stdin.is_terminal() { // v1.70.0 later
        let text = io::read_to_string(stdin).expect("Can not read stdin");
        println!("{}", text);
    }
}
Mezoff answered 13/4 at 4:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.