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 into
main.rs
和lib.rs
, put the business logic intolib.rs
- When command line parsing logic is low, put it in
main.rs
To also go - When the command line parsing logic becomes complex, it needs to be removed from
main.rs
To 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:
- Write a test that will fail, run it, and make sure it fails for the expected reason
- Write or change just enough code to make the new test pass
- Refactor the code you just added or modified to ensure that the tests always pass
- 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