I/O: stdin and File Access
Goal of This Episode
Meet Rust’s I/O (input / output) methods and learn to read and write files.
Concept
We’ve learned to express file locations with Path and to work with string content — this episode covers actually reading and writing files.
stdin Revisited
In Chapter 1 we copied these three lines verbatim to read user input:
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).expect("read failed");
let name = input.trim();
}
You’ve since learned String, &mut, and .expect(), so this should all make sense now. What we haven’t mentioned: std::io::stdin() returns a Stdin struct, and read_line returns an io::Result<usize> — a type alias for Result<usize, io::Error> that many I/O functions in the standard library use.
The Simplest File Reading and Writing
fs::read_to_string’s parameter type is impl AsRef<Path> — the AsRef from Episode 1 pays off here. You can pass a &str, String, &Path, or PathBuf, no manual conversion needed.
Reading a whole file into a string:
use std::fs;
fn main() {
let content = fs::read_to_string("hello.txt").expect("read failed");
println!("{}", content);
}
Writing to a file (created if missing, overwritten if present):
use std::fs;
fn main() {
fs::write("output.txt", "Hello, file!").expect("write failed");
}
These two functions are wonderfully simple, but read_to_string reads the entire file into memory at once — not a good fit for very large files.
File + BufReader: Reading Line by Line
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() {
let file = File::open("data.txt").expect("open failed");
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line.expect("failed to read line");
println!("{}", line);
}
}
File::open opens the file, BufReader wraps it to provide a buffer, and .lines() reads line by line — each line is an io::Result<String>.
Writing to a File
use std::fs::File;
use std::io::Write;
fn main() {
let mut file = File::create("output.txt").expect("failed to create");
writeln!(file, "first line").expect("write failed");
writeln!(file, "second line").expect("write failed");
}
File::create creates (or overwrites) a file, and writeln! is a lot like println!, just with the output going to the file instead of the screen. Using writeln! requires importing the Write trait.
Read, Write, BufRead
The standard library abstracts I/O with traits:
Read: things bytes can be read from (File,Stdin,TcpStream, etc.).Write: things bytes can be written to (File,Stdout,TcpStream, etc.).BufRead: buffered reading, providinglines()and other methods.BufReadercan turn anyReadinto aBufRead.
That’s why many functions declare parameters as impl Read or impl Write — whether you pass a file, stdin, or a network connection, anything implementing the right trait works.
Example Code
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
fn main() -> io::Result<()> {
// write a file
let mut file = File::create("names.txt")?;
writeln!(file, "Alice")?;
writeln!(file, "Bob")?;
writeln!(file, "Charlie")?;
// read the whole file at once
let all = fs::read_to_string("names.txt")?;
println!("whole file:\n{}", all);
// read line by line
let file = File::open("names.txt")?;
let reader = BufReader::new(file);
for (i, line) in reader.lines().enumerate() {
println!("line {}: {}", i + 1, line?);
}
Ok(())
}
Recap
io::Result<T>is a type alias forResult<T, io::Error>.fs::read_to_string/fs::write: the simplest one-line read/writeFile::open+BufReader: read big files line by lineFile::create+writeln!: write line by line.Read,Write, andBufReadare the core I/Otraits, giving different sources (files,stdin, the network) one shared interface.