Read and Write Files

RustBeginner
Practice Now

Introduction

Many useful programs cross a filesystem boundary: they read existing data, process it, and save a result. Those operations can fail, so Rust's file APIs return Result values rather than pretending success is guaranteed.

You will inspect a prepared service log, represent file locations with Path, read text with std::fs, build a small summary, write it to another file, and propagate I/O errors with ?.

Read a Text File Through a Path

In this step, you will inspect a prepared log and read it into a Rust String.

The project is /home/labex/project/log-summary. It contains data/events.log as input, an empty output directory for the later report, and src/main.rs as the program source. Enter the project:

cd /home/labex/project/log-summary

Open the input with nano so you know what the program will read:

nano data/events.log

The file contains three short records. Do not change it; press Ctrl+X to exit. Now open the Rust source:

nano src/main.rs

The two prepared use lines make std::fs file functions and the Path type available by short names. They are import plumbing rather than this Lab's main topic; the later modules Lab explains paths and use declarations fully. The return type std::io::Result<()> says main either completes with the empty success value () or returns an I/O error. The final Ok(()) is the successful outcome.

Replace the Step 1 comment with:

    let input_path = Path::new("data/events.log");
    let contents = fs::read_to_string(input_path)?;
    println!("Input: {}", input_path.display());
    println!("Contents:\n{}", contents.trim_end());

Path::new creates a borrowed filesystem-path view without opening anything. The relative path is resolved from the project directory where you run Cargo. fs::read_to_string opens the file, reads UTF-8 text into an owned String, and returns a Result. The ? produces the String on success or returns the I/O error from main. display() formats a path for people, and trim_end() prevents the file's final newline from creating an extra blank output line.

Save with Ctrl+O, press Enter, and exit with Ctrl+X. Check and run:

cargo check
cargo run --quiet
Input: data/events.log
Contents:
INFO service started
WARN disk nearing limit
INFO backup complete

The displayed path and all three records prove the program read the intended file.

Count Records in the Loaded Text

In this step, you will count logical lines in the String already held in memory.

The lines() method creates an iterator over text lines without changing the source string. Its count() consumer returns how many items the iterator produced. Open the source:

nano src/main.rs

Replace the Step 2 comment with:

    let line_count = contents.lines().count();
    println!("Line count: {line_count}");

Save and exit, then run:

cargo run --quiet

The new final line should be:

Line count: 3

This works whether or not the input ends with a newline because lines() identifies logical records rather than counting newline bytes.

Build a Stable Summary String

In this step, you will count warning records and format both counts into text that is ready to save.

The starts_with string method tests a prefix. Combining it with the iterator adapters you already know lets the program count only lines beginning with WARN. Open the source:

nano src/main.rs

Replace the Step 3 comment with:

    let warning_count = contents
        .lines()
        .filter(|line| line.starts_with("WARN"))
        .count();
    let summary = format!("lines: {line_count}\nwarnings: {warning_count}\n");
    println!("Summary ready:\n{}", summary.trim_end());

The filter predicate keeps warning lines, and count() consumes that filtered iterator. format! returns a new String instead of printing it; the explicit \n characters create two lines and leave a final newline suitable for a text file.

Save and exit, then check and run:

cargo check
cargo run --quiet

The final section should be:

Summary ready:
lines: 3
warnings: 1

The summary is now data in memory, ready for the filesystem boundary.

Write and Reread the Summary File

In this step, you will write the summary to a prepared output directory and read it back as evidence.

fs::write(path, data) creates a file or completely replaces its old contents with the supplied bytes; it does not append. Like reading, writing can fail and returns an I/O Result, so you will propagate it with ?. Open the source:

nano src/main.rs

Replace the Step 4 comment with:

    let output_path = Path::new("output/summary.txt");
    fs::write(output_path, &summary)?;
    let saved = fs::read_to_string(output_path)?;
    println!("Saved {}:\n{}", output_path.display(), saved.trim_end());

&summary borrows the summary while write reads its bytes, so the String remains owned by the program. Reading the file back checks the actual saved artifact instead of assuming the write worked.

Save and exit, then check and run:

cargo check
cargo run --quiet

The final section should be:

Saved output/summary.txt:
lines: 3
warnings: 1

For one final inspection, open the created file:

nano output/summary.txt

You should see the same two summary lines. Press Ctrl+X to exit without changes.

Summary

You represented file locations with Path, read UTF-8 text with fs::read_to_string, propagated I/O errors with ?, summarized loaded records, wrote the result with fs::write, and reread the saved file to confirm its contents.