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.
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");
}id,total
1,120.50
3,15.00The 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().
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(())
}$ 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.
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()
}row 1: 1
row 2: 4
row 3: 9
row 4: 16
row 5: 25Part 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.
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(())
}$ printf '10\n20\nabc\n12\n' | cargo run
not a number: "abc"
total: 425. 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.
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())?;$ cat poem.txt | cargo run
longest line: Violets are blue6. 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.
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(())
}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.
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())
}= 5
= 20
[20.0]
error: need two numbers
error: unknown command "sqrt"
byeNote 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.
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");
}
}interactive: false, use colors: false
reading input from a pipe or fileThis 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.
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);
}
}
}$ cargo run | head -3
line 1
line 2
line 3With 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.
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());
}
}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.00sPart 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.
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())
} 3 /home
2 /login
1 /cart12. 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.
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())
}NAME STATUS RESTARTS AGE
api-7f9c Running 0 3d
worker-1 CrashLoopBackOff 12 5h
db Running 1 21d13. 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.
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())
}alpha: 5
beta: 4
gamma: 5
delta: 5Here 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.
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(())
}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 aBufWriter. read_linereturns0at 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 BufReadandimpl Write; pass the real streams inmain, and bytes andVec<u8>in tests. IsTerminaltells you whether a person is on the other end. HandleBrokenPipein tools that print a lot.Box<dyn Write>chooses between stdout and a file at runtime.
