Guide contents

Lesson 54 of 94

Standard Input, Output & Error

Every program starts with three open streams: standard input (what's typed or piped in), standard output (the program's results), and standard error (messages for the person running it). Rust exposes them as io::stdin(), io::stdout(), and io::stderr(). They implement the same Read, BufRead, and Write traits as files, so everything from Reading & Writing Files applies.

Task Rust
Print a line to stdout println!("{x}")
Print a line to stderr eprintln!("{x}")
Flush stdout io::stdout().flush()?
Read one line io::stdin().read_line(&mut line)?
Iterate over lines io::stdin().lines()
Check for a terminal io::stdin().is_terminal()

Part 1: Output

1. Results on stdout, messages on stderr

Keep the two apart: stdout carries the program's actual output, which may be piped into another program or saved to a file, and stderr carries warnings, errors, and progress for the human watching.

Rust
fn main() {
    println!("id,total"); // stdout: the program's result
    eprintln!("warning: skipped 2 rows with a missing total"); // stderr: for the user
    println!("1,120.50");
    println!("3,15.00");
}
Output
id,total
1,120.50
3,15.00

The output above shows only stdout, because that's what a pipe or > captures. Run cargo run > totals.csv, and the file contains the three data lines while the warning still appears in the terminal. Mixing warnings into stdout would corrupt the CSV.

2. Prompts need a flush

print! doesn't end the line, and stdout only sends its buffer to the terminal at the end of each line. A prompt printed with print! therefore stays invisible until you call flush().

Rust
use std::io::{self, Write};

fn main() -> io::Result<()> {
    print!("Your name: ");
    io::stdout().flush()?; // without this, the prompt might not appear before input is read

    let mut name = String::new();
    io::stdin().read_line(&mut name)?;
    println!("Hello, {}!", name.trim());
    Ok(())
}
Text
$ cargo run
Your name: Ana
Hello, Ana!

flush comes from the Write trait, so it needs use std::io::Write.

3. Writing a lot of output quickly

Each println! locks stdout, formats, and writes separately. For thousands of lines, lock it once and wrap it in a BufWriter. The output is the same; the program can be many times faster.

Rust
use std::io::{self, BufWriter, Write};

fn main() -> io::Result<()> {
    let mut out = BufWriter::new(io::stdout().lock()); // lock once, buffer many lines
    for i in 1..=5 {
        writeln!(out, "row {i}: {}", i * i)?;
    }
    out.flush()
}
Output
row 1: 1
row 2: 4
row 3: 9
row 4: 16
row 5: 25

Part 2: Input

4. Reading one line

read_line appends the next line, including its \n, to a String and returns the number of bytes read. 0 means the input has ended: the user pressed Ctrl-D (Ctrl-Z on Windows), or the piped file is exhausted. Most programs trim() the line before using it.

Rust
use std::io;

fn main() -> io::Result<()> {
    let mut total = 0;
    loop {
        let mut line = String::new();
        if io::stdin().read_line(&mut line)? == 0 {
            break; // end of input
        }
        match line.trim().parse::<i32>() {
            Ok(n) => total += n,
            Err(_) => eprintln!("not a number: {:?}", line.trim()),
        }
    }
    println!("total: {total}");
    Ok(())
}
Text
$ printf '10\n20\nabc\n12\n' | cargo run
not a number: "abc"
total: 42

5. Reading every line, or all of it at once

io::stdin().lines() is the loop from the previous example as an iterator. When the input is small, io::read_to_string(io::stdin()) reads all of it into one String.

Rust
use std::io;

fn main() -> io::Result<()> {
    // Option 1: line by line, for input of any size.
    let mut longest = String::new();
    for line in io::stdin().lines() {
        let line = line?;
        if line.len() > longest.len() {
            longest = line;
        }
    }
    println!("longest line: {longest}");
    Ok(())
}

// Option 2, when the whole input fits in memory:
// let text = io::read_to_string(io::stdin())?;
Text
$ cat poem.txt | cargo run
longest line: Violets are blue

6. Testable programs: logic over BufRead and Write

Code that calls io::stdin() directly can only be tested by piping text into the compiled program. Put the logic in a function that takes impl BufRead and impl Write instead; main passes the real streams, and tests pass bytes and a Vec<u8>. This one also takes a separate writer for warnings, which main would connect to stderr.

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

fn sum_numbers(input: impl BufRead, mut out: impl Write, mut warnings: impl Write) -> io::Result<()> {
    let mut total = 0.0;
    for (index, line) in input.lines().enumerate() {
        let line = line?;
        let text = line.trim();
        if text.is_empty() {
            continue;
        }
        match text.parse::<f64>() {
            Ok(n) => total += n,
            Err(_) => writeln!(warnings, "line {}: not a number: {text:?}", index + 1)?,
        }
    }
    writeln!(out, "total: {total}")
}

fn main() -> io::Result<()> {
    // The real program: sum_numbers(io::stdin().lock(), io::stdout().lock(), io::stderr().lock())
    let mut warnings = Vec::new();
    sum_numbers("12.5\n7\n\nabc\n0.5\n".as_bytes(), io::stdout().lock(), &mut warnings)?;
    print!("warnings:\n{}", String::from_utf8_lossy(&warnings));
    Ok(())
}
Output
total: 20
warnings:
line 4: not a number: "abc"

&mut Vec<u8> implements Write, so the caller keeps the vector and can read what was written.

7. An interactive calculator

A small read-eval-print loop: a stack calculator that reads one token per line. Because it's written against BufRead and Write, the example runs it on a scripted session.

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

fn calculator(input: impl BufRead, mut out: impl Write) -> io::Result<()> {
    let mut stack: Vec<f64> = Vec::new();
    for line in input.lines() {
        let line = line?;
        let reply = match line.trim() {
            "" => continue,
            "quit" => break,
            "show" => format!("{stack:?}"),
            op @ ("+" | "-" | "*" | "/") => match (stack.pop(), stack.pop()) {
                (Some(b), Some(a)) => {
                    let result = match op {
                        "+" => a + b,
                        "-" => a - b,
                        "*" => a * b,
                        _ => a / b,
                    };
                    stack.push(result);
                    format!("= {result}")
                }
                _ => String::from("error: need two numbers"),
            },
            token => match token.parse::<f64>() {
                Ok(n) => {
                    stack.push(n);
                    continue;
                }
                Err(_) => format!("error: unknown command {token:?}"),
            },
        };
        writeln!(out, "{reply}")?;
    }
    writeln!(out, "bye")
}

fn main() -> io::Result<()> {
    // The real program: calculator(io::stdin().lock(), io::stdout().lock())
    let session = "2\n3\n+\n4\n*\nshow\n+\nsqrt\nquit\n5\n";
    calculator(session.as_bytes(), io::stdout().lock())
}
Output
= 5
= 20
[20.0]
error: need two numbers
error: unknown command "sqrt"
bye

Note that the stack ends up empty after the failed +: both pop() calls ran before the match checked them. A production calculator would check stack.len() first.

8. Is anyone typing?

IsTerminal tells you whether a stream is connected to a terminal or redirected to a file or pipe. Tools use it to decide whether to print colors, show progress bars, or prompt for input.

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

fn main() {
    let interactive = io::stdin().is_terminal();
    let colors = io::stdout().is_terminal();
    println!("interactive: {interactive}, use colors: {colors}");
    if !interactive {
        println!("reading input from a pipe or file");
    }
}
Output
interactive: false, use colors: false
reading input from a pipe or file

This output was captured with both streams redirected, which is why it says false. Run it directly in a terminal, and both are true.

9. Handling a closed pipe

In cargo run | head -3, head exits after three lines, and further writes to stdout fail with a "broken pipe" error. println! turns that error into a panic. A command-line tool that writes lots of output should use writeln! and stop quietly on BrokenPipe.

Rust
use std::io::{self, ErrorKind, Write};
use std::process;

fn main() {
    let mut out = io::stdout().lock();
    for i in 1.. {
        if let Err(e) = writeln!(out, "line {i}") {
            if e.kind() == ErrorKind::BrokenPipe {
                return; // the reader went away: that's not an error for us
            }
            eprintln!("error: {e}");
            process::exit(1);
        }
    }
}
Text
$ cargo run | head -3
line 1
line 2
line 3

With println! instead, the same command prints failed printing to stdout: Broken pipe and a panic message.

10. Testing output with Vec<u8>

Unit tests for a function over BufRead and Write need no files and no processes: feed it bytes, collect the output in a Vec<u8>, and compare strings.

Rustcargo test
use std::io::{self, BufRead, Write};

fn greet_all(input: impl BufRead, mut out: impl Write) -> io::Result<usize> {
    let mut count = 0;
    for line in input.lines() {
        let line = line?;
        let name = line.trim();
        if !name.is_empty() {
            writeln!(out, "Hello, {name}!")?;
            count += 1;
        }
    }
    Ok(count)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greets_each_name() {
        let mut out = Vec::new();
        let n = greet_all("Ana\n\n  Ben  \n".as_bytes(), &mut out).unwrap();
        assert_eq!(n, 2);
        assert_eq!(String::from_utf8(out).unwrap(), "Hello, Ana!\nHello, Ben!\n");
    }

    #[test]
    fn empty_input_greets_nobody() {
        let mut out = Vec::new();
        assert_eq!(greet_all("".as_bytes(), &mut out).unwrap(), 0);
        assert!(out.is_empty());
    }
}
Output
running 2 tests
test tests::empty_input_greets_nobody ... ok
test tests::greets_each_name ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Part 3: Realistic examples

11. Realistic example: sort | uniq -c

Count how often each line occurs and print the counts, most frequent first. It's the kind of small filter you can chain with other tools in a shell pipeline.

Rust
use std::collections::HashMap;
use std::io::{self, BufRead, Write};

fn count_lines(input: impl BufRead, mut out: impl Write) -> io::Result<()> {
    let mut counts: HashMap<String, usize> = HashMap::new();
    for line in input.lines() {
        *counts.entry(line?).or_insert(0) += 1;
    }
    let mut sorted: Vec<(String, usize)> = counts.into_iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
    for (line, count) in sorted {
        writeln!(out, "{count:>7} {line}")?;
    }
    Ok(())
}

fn main() -> io::Result<()> {
    // The real program: count_lines(io::stdin().lock(), io::stdout().lock())
    let access_log_paths = "/home\n/login\n/home\n/cart\n/home\n/login\n";
    count_lines(access_log_paths.as_bytes(), io::stdout().lock())
}
Output
      3 /home
      2 /login
      1 /cart

12. Realistic example: aligning columns

A filter that reads whitespace-separated columns and prints them aligned, like the Unix column -t. It needs to see every row before printing any, to know each column's width.

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

fn align(input: impl BufRead, mut out: impl Write) -> io::Result<()> {
    let mut rows: Vec<Vec<String>> = Vec::new();
    for line in input.lines() {
        let line = line?;
        let cells: Vec<String> = line.split_whitespace().map(String::from).collect();
        if !cells.is_empty() {
            rows.push(cells);
        }
    }

    let columns = rows.iter().map(Vec::len).max().unwrap_or(0);
    let widths: Vec<usize> =
        (0..columns).map(|c| rows.iter().filter_map(|row| row.get(c)).map(|cell| cell.chars().count()).max().unwrap_or(0)).collect();

    for row in &rows {
        let padded: Vec<String> = row.iter().zip(&widths).map(|(cell, w)| format!("{cell:<w$}")).collect();
        writeln!(out, "{}", padded.join("  ").trim_end())?;
    }
    Ok(())
}

fn main() -> io::Result<()> {
    let input = "NAME STATUS RESTARTS AGE\napi-7f9c Running 0 3d\nworker-1 CrashLoopBackOff 12 5h\ndb Running 1 21d\n";
    align(input.as_bytes(), io::stdout().lock())
}
Output
NAME      STATUS            RESTARTS  AGE
api-7f9c  Running           0         3d
worker-1  CrashLoopBackOff  12        5h
db        Running           1         21d

13. Realistic example: progress on stderr, data on stdout

A long-running command that reports progress to stderr and writes its results to stdout. Redirect stdout to a file, and the progress stays visible in the terminal without ending up in the data.

Rust
use std::io::{self, Write};
use std::thread;
use std::time::Duration;

fn process(items: &[&str], mut out: impl Write, mut progress: impl Write) -> io::Result<()> {
    for (i, item) in items.iter().enumerate() {
        thread::sleep(Duration::from_millis(20)); // pretend to work
        writeln!(out, "{item}: {}", item.len())?;
        write!(progress, "\rprocessed {}/{}", i + 1, items.len())?; // \r returns to the start of the line
        progress.flush()?;
    }
    writeln!(progress)?;
    Ok(())
}

fn main() -> io::Result<()> {
    let items = ["alpha", "beta", "gamma", "delta"];
    process(&items, io::stdout().lock(), io::stderr().lock())
}
Output
alpha: 5
beta: 4
gamma: 5
delta: 5

Here the output shows the stdout lines; in a terminal, the processed 4/4 line updates in place on stderr underneath. Crates such as indicatif draw full progress bars the same way.

14. Choosing where output goes at runtime

Some tools write to a file when given one and to stdout otherwise, the -o option of many Unix tools. A Box<dyn Write> holds either one.

Rust
use std::fs::{self, File};
use std::io::{self, BufWriter, Write};

fn open_output(path: Option<&str>) -> io::Result<Box<dyn Write>> {
    Ok(match path {
        Some(p) => Box::new(BufWriter::new(File::create(p)?)),
        None => Box::new(io::stdout().lock()),
    })
}

fn export(rows: &[(&str, u32)], path: Option<&str>) -> io::Result<()> {
    let mut out = open_output(path)?;
    for (name, qty) in rows {
        writeln!(out, "{name},{qty}")?;
    }
    out.flush()
}

fn main() -> io::Result<()> {
    let rows = [("bolts", 120), ("nuts", 80)];
    export(&rows, None)?; // like running without -o
    export(&rows, Some("export.csv"))?; // like -o export.csv
    println!("export.csv: {:?}", fs::read_to_string("export.csv")?);
    Ok(())
}
Output
bolts,120
nuts,80
export.csv: "bolts,120\nnuts,80\n"

Summary

  • println! writes to stdout, for results; eprintln! writes to stderr, for messages. Keep them apart so output can be piped and redirected.
  • Flush after print! prompts. For lots of output, lock stdout once and wrap it in a BufWriter.
  • read_line returns 0 at the end of input and keeps the newline; io::stdin().lines() iterates over lines; io::read_to_string(io::stdin()) reads everything.
  • Write program logic against impl BufRead and impl Write; pass the real streams in main, and bytes and Vec<u8> in tests.
  • IsTerminal tells you whether a person is on the other end. Handle BrokenPipe in tools that print a lot.
  • Box<dyn Write> chooses between stdout and a file at runtime.

Next: Command-Line Arguments, Environment & Processes.