changed 5 years ago
Published Linked with GitHub

Rust Study Session #11

Ch. 12 (part 1): Building a Command Line Program

2020.10.16 - Salvatore La Bua


I/O Project: Implementing grep

  • Organising code (Ch. 7)
  • Using vectors and strings (Ch. 8)
  • Handling errors (Ch. 9)
  • Using traits and lifetimes (Ch. 10)
  • Writing tests (Ch. 11)

Summary

  • Accepting Command Line Arguments
  • Reading a File
  • Refactoring

Command Line Arguments

Creating the project

$ cargo new minigrep Created binary (application) `minigrep` project $ cd minigrep

Command line call example

$ cargo run searchstring example-filename.txt

Reading the Argument Values

  • std::env::args is a function from the standard library that returns an iterator.
  • An iterator can be converted to a collection with the .collect() method.
use std::env; // a good practice is to import the parent module fn main() { let args: Vec<String> = env::args().collect(); println!("{:?}", args); }

※ For invalid unicode, use std::env::args_os instead.


Reading the Argument Values

Running the program without arguments

$ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/minigrep` ["target/debug/minigrep"]

Running the program with two arguments

$ cargo run needle haystack Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 1.57s Running `target/debug/minigrep needle haystack` ["target/debug/minigrep", "needle", "haystack"]

Saving the Argument Values in Variables

use std::env; fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let filename = &args[2]; println!("Searching for {}", query); println!("In file {}", filename); }

Running the program

$ cargo run test sample.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep test sample.txt` Searching for test In file sample.txt

Reading a File

Test file

I’m nobody! Who are you?
Are you nobody, too?
Then there’s a pair of us - don’t tell!
They’d banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!


Reading a File

The code to read a file from disk

use std::env; use std::fs; fn main() { // --snip-- let args: Vec<String> = env::args().collect(); let query = &args[1]; let filename = &args[2]; println!("Searching for {}", query); println!("In file {}", filename); let contents = fs::read_to_string(filename) .expect("Something went wrong reading the file"); println!("With text:\n{}", contents); }

Reading a file

Running the program

$ cargo run the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: I’m nobody! Who are you? Are you nobody, too? Then there’s a pair of us - don’t tell! They’d banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog!

Refactoring

Improve modularity

  • Separating functionalities
  • Grouping configuration variables

Error handling

  • Customise error messages
  • Grouping error handling code

Separating functionalities

When more functions are being added to the main program:
Split program into main.rs and lib.rs.

  • main.rs
    • Command line parsing.
    • Configuration set-up.
    • Calling a run function in lib.rs.
    • Handling errors if run returns an error.
  • lib.rs
    • Any of the program's logic.

Separating functionalities

Applying the separation

fn main() { let args: Vec<String> = env::args().collect(); let (query, filename) = parse_config(&args); // --snip-- } fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let filename = &args[2]; (query, filename) }

Grouping configuration variables

Creating a structure for the configuration

fn main() { let args: Vec<String> = env::args().collect(); let config = parse_config(&args); println!("Searching for {}", config.query); println!("In file {}", config.filename); let contents = fs::read_to_string(config.filename) .expect("Something went wrong reading the file"); // --snip-- } struct Config { query: String, filename: String, } fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } }

Grouping configuration variables

Creating a constructor for Config

fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args); // --snip-- } // --snip-- impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } } }

Error handling

Customise error messages

$ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21 note : run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Error handling

Customise error messages

struct Config { query: String, filename: String, } impl Config { fn new(args: &[String]) -> Config { if args.len() < 3 { panic!("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } } }

Error handling

Customise error messages

$ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'not enough arguments', src/main.rs:26:13 note : run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Error handling

Returning a Result

impl Config { fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); Ok(Config { query, filename }) } }

Error handling

Calling Config::new

use std::process; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {}", err); process::exit(1); }); // --snip--
$ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/minigrep` Problem parsing arguments: not enough arguments

Separating functionalities

Extracting logic from main

fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.filename); run(config); } fn run(config: Config) { let contents = fs::read_to_string(config.filename) .expect("Something went wrong reading the file"); println!("With text:\n{}", contents); } // --snip--

Error handling

Returning Errors from the run Function

use std::error::Error; // --snip-- fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; println!("With text:\n{}", contents); Ok(()) }

Error handling

Returning Errors from the run Function

$ cargo run the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) warning: unused `std::result::Result` that must be used --> src/main.rs:19:5 | 19 | run(config); | ^^^^^^^^^^^^ | = note : `#[warn(unused_must_use)]` on by default = note : this `Result` may be an `Err` variant, which should be handled Finished dev [unoptimized + debuginfo] target(s) in 0.71s Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: I’m nobody! Who are you? Are you nobody, too? Then there’s a pair of us - don’t tell! They’d banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog!

Error handling

Errors returned from run to main

fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.filename); if let Err(e) = run(config) { println!("Application error: {}", e); process::exit(1); } }

Separating functionalities

Splitting code into a library crate

Move the code that is not in the main function to lib.rs:

  • The relevant use statements
  • The definition of Config
  • The Config::new function definition
  • The run function definition

Separating functionalities

The library crate src/lib.rs

// Relevant use statements use std::error::Error; use std::fs; // Definition of Config pub struct Config { pub query: String, pub filename: String, } // Config::new function definition impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { // --snip-- } } // run function definition pub fn run(config: Config) -> Result<(), Box<dyn Error>> { // --snip-- }

Separating functionalities

Importing the library crate into main.rs

//src/main.rs use std::env; use std::process; use minigrep::Config; fn main() { // --snip-- if let Err(e) = minigrep::run(config) { // --snip-- } }

References

From The Rust Programming Language book:

  • Ch. 12: An I/O Project:
    Building a Command Line Program [EN] [JP]

Select a repo