IS4010: AI-Enhanced Application Development

Week 12: Building a Real Rust Program

Brandon M. Greenwell

Session 1: Error handling & crates

Recoverable vs. unrecoverable errors

  • Rust groups errors into two major categories.
  • Unrecoverable errors are serious problems where there’s no way to proceed. These errors cause a panic!, which immediately stops the program.
  • Recoverable errors are expected issues, like a file not being found. These are handled using the Result<T, E> enum, which lets a function return either a success value (Ok) or an error value (Err).

The Result and Option enums

  • Result and Option are the most important enums for handling potential failures in Rust.
  • Option<T> is used when a value could be something (Some(T)) or nothing (None). Think of it as preventing null pointer errors.
  • Result<T, E> is for operations that can fail. It returns Ok(T) on success or Err(E) on failure.
  • We can use a match expression to handle every possible variant of these enums.

The ? operator for cleaner error handling

  • Using match to handle every error can be verbose. The question mark (?) operator is a powerful shortcut for propagating errors.
  • When placed at the end of an expression that returns a Result, ? does two things:
    • If the Result is Ok, it unwraps the value and continues.
    • If the Result is Err, it immediately returns the Err from the whole function.
  • It can only be used in functions that return a Result or Option.

Using external libraries (crates)

  • crates.io is the official registry for Rust’s open-source libraries, called crates.
  • To use an external crate, you add it as a dependency in your Cargo.toml file under the [dependencies] section.
  • When you run cargo build, Cargo automatically downloads and compiles all your project’s dependencies.
# In Cargo.toml
[dependencies]
rand = "0.8.5" 
# "rand" is the crate name, "0.8.5" is the version

Session 2: Building a command-line app

Reading command-line arguments

  • We can access command-line arguments passed to our program using the std::env::args() function.
  • This function returns an iterator over the arguments. We can collect() these into a Vec<String> (a vector of strings) to easily access them.
  • The first argument (at index 0) is always the name of the program itself.
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("My path is {}.", args[0]);
}

Reading file contents

  • Rust’s standard library provides the std::fs module for file system operations.
  • The fs::read_to_string() function is a simple way to read the entire contents of a file into a string.
  • Because this operation can fail (e.g., if the file doesn’t exist), this function returns a Result<String, std::io::Error>.
use std::fs;

fn main() {
    let contents = fs::read_to_string("poem.txt")
        .expect("Something went wrong reading the file");
    
    println!("File contents:\n{}", contents);
}

Searching for a query

  • Once we have the file contents as a single string, we can process it.
  • The .lines() method gives us an iterator over the lines of the string.
  • We can then loop through each line and use the .contains() string method to check if it includes our search query.
  • We’ll collect all matching lines into a new vector to display as our result.

Introducing lab 12

  • This week’s lab brings everything together: you will build a simple version of the popular command-line tool grep.
  • Your program will take two command-line arguments: a search query and a filename.
  • It will read the specified file, search for the query, and print only the lines that contain the query.
  • Full instructions are in labs/lab12/README.md.