The target

We need to develop a command line tool, a mini grep, grep (global Search regular expression and print, global search regular expression and print) command in Linux to find the string in the file that matches the conditions.

All we need to do is take the filename and string as arguments, then read the contents of the file to search for the line containing the specified string, and print it out, typing the following command:

cargo run xxx xxx.txt
Copy the code

Key steps of implementation

Project creation

# Project name Minigrep
cargo new minigrep

cd minigrep
Copy the code

Read command line arguments

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect(); // To receive arguments entered on the command line
    
    let query = &args[1];
    let filename = &args[2];

    println!("{:? }", args);
    println!("Search for {}", query);
    println!("In file {}", filename);
}
Copy the code

Run:

cargo run xxx xxx.txt
Copy the code

Output:

["target/debug/minigrep", "xxx", "xxx.txt"]
Search for xxx
In file xxx.txt
Copy the code

Read the file

use std::env;
use std::fs; // Introduce the standard library for reading files

fn main() {
    let args: Vec<String> = env::args().collect();
    
    let query = &args[1];
    let filename = &args[2];

    let contents = fs::read_to_string(filename) // Read files as strings
    .expect("Something went wrong reading the file"); // Processing error

    println!("With text:\n{}", contents);
}
Copy the code

Create a poem. TXT in the minigrep root directory and write some random text.

run

cargo run xxx poem.txt
Copy the code

This will output the contents of poems. TXT.

refactoring

Now main.rs has too much to do, parsing parameters, reading files, and error handling is not very clear. Now the program is simple, but later on as the project becomes more complex, the program will be difficult or impossible to maintain, so we need to split responsibilities.

Guiding principles for separation of concerns in binary programs:

  • Split the program intomain.rslib.rs, put the business logic intolib.rs
  • When command line parsing logic is low, put it inmain.rsTo also go
  • When the command line parsing logic becomes complex, it needs to be removed frommain.rsTo extractlib.rs

Command line parameters read partial refactoring

Create lib.rs and use struct to handle parameters to make the code easier to understand.

// lib.rs
pub struct Config {
    pub query: String.pub filename: String,}impl Config {
    pub fn new(args: &Vec<String>) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}
Copy the code

The call to main.rs also needs to be modified.

// main.rs
use std::env;
use std::fs;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();
    
    let config = Config::new(&args);

    let contents = fs::read_to_string(config.filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}
Copy the code

Refactoring of the error handling part

When errors occur, the information printed in the command line is still complex and redundant. Many information that users do not care about is also printed out, for example: Thread ‘main’ panicked at ‘Something went wrong reading the file: Os {code: 2, kind: NotFound, message: “System could not find the specified file.” }’, src\main.rs:10:6 … .

We can use the Result enumeration for error handling and modify the handling of the new function.

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

        Ok(Config { query, filename })
    }
}
Copy the code

Modify main.rs Config return handling.

// main.rs
use std::env;
use std::fs;
use std::process;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();
    
    // unwrap_or_else Returns data on success, and calls a closure on error.
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(0);
    });

    let contents = fs::read_to_string(config.filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}
Copy the code

Extracting the Run function further simplifies the logic of main.rs

The run function is the piece of logic that handles file reading, and we extract it.

// lib.rs
// We used expect for error processing, which would cause panic, so we need to change it to Result
pub fn run(config: Config) -> Result< (),Box<dyn Error>> { // Box
    letcontents = fs::read_to_string(config.filename)? ;/ /? Propagate an error, returning the error to the caller of the function

    println!("With text:\n{}", contents);
    Ok(())}Copy the code
// main.rs
use std::env;
use std::process;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();
    
    // unwrap_or_else Returns data on success, and calls a closure on error.
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(0);
    });

    if let Err(e) = minigrep::run(config) {
        println!("Application error: {}", e);
        process::exit(0); }}Copy the code

Write library functionality using TDD

TDD (Test-driven Development), we will use this approach to develop search keyword capabilities.

The general steps of TDD are as follows:

  1. Write a test that will fail, run it, and make sure it fails for the expected reason
  2. Write or change just enough code to make the new test pass
  3. Refactor the code you just added or modified to ensure that the tests always pass
  4. Return to Step 1 and continue

Add test cases

// lib.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents)); }}Copy the code

Add a search function

// lib.rs
pub fn search<'a>(query: &'a str, contents: &'a str) - >VecThe < &'a str> {
    let mut results = Vec::new();
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}
Copy the code

Modify the run function:

pub fn run(config: Config) -> Result< (),Box<dyn Error>> {
    letcontents = fs::read_to_string(config.filename)? ;for line in search(&config.query, &contents) {
      println!("{}", line);
    }
    Ok(())}Copy the code

Using environment variables

Use environment variables to implement case-sensitive search keywords.

Also TDD development, so first modify the test case:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duck tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:"."Trust me."], search_case_insensitive(query, contents) ); }}Copy the code

Then add the search_case_insensitive function:

pub fn search_case_insensitive<'a>(query: &'a str, contents: &'a str) - >VecThe < &'a str> {
    let mut results = Vec::new();
    let query = query.to_lowercase();
    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}
Copy the code

Cargo test is executed to ensure that the use case passes.

Then modify Config:

use std::env; / / add

pub struct Config {
    pub query: String.pub filename: String.pub case_sensitive: bool,}impl Config {
    pub fn new(args: &Vec<String- > >)Result<Config, &str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }
        let query = args[1].clone();
        let filename = args[2].clone();
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
        Ok(Config { query, filename, case_sensitive })
  }
}
Copy the code

Modify the run function:

pub fn run(config: Config) -> Result< (),Box<dyn Error>> {
    letcontents = fs::read_to_string(config.filename)? ;let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };
    for line in results {
      println!("{}", line);
    }
    Ok(())}Copy the code

Execute CASE_INSENSITIVE=1 cargo run rUst Poems. TXT to enable the environment variable (Windows requires a set uppermost).

Output error information to standard error

Stdout -> println!

Standard error: stderr -> eprintln!

TXT to output. TXT. If no code changes are made, then output. TXT contains both normal output and error messages. – > eprintln!) Error messages will not be printed to output.txt.

// main.rs
fn main() {
    let args: Vec<String> = env::args().collect();
    
    // unwrap_or_else Returns data on success, and calls a closure on error.
    letconfig = Config::new(&args).unwrap_or_else(|err| { eprintln! ("Problem parsing arguments: {}", err);
        process::exit(0);
    });

    if let Err(e) = minigrep::run(config) { eprintln! ("Application error: {}", e);
        process::exit(0); }}Copy the code