12.7.0 Before We Begin
Chapter 12 builds a sample project: a command-line program. The program is grep (Global Regular Expression Print), a tool for global regular-expression searching and output. Its function is to search for specified text in a specified file.
This project is divided into these steps:
- Receiving command-line arguments
- Reading files
- Refactoring: improving modules and error handling
- Using TDD (test-driven development) to develop library functionality
- Using environment variables (this article)
- Writing error messages to standard error instead of standard output
If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
12.7.1 Review
Here is all the code written up to the previous article.
lib.rs:
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub 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,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[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));
}
}
main.rs:
use std::env;
use std::process;
use minigrep::Config;
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);
});
if let Err(e) = minigrep::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
In these earlier sections, we completed the move of the business logic into lib.rs. That is very helpful for writing tests, because the logic in lib.rs can be called directly with different arguments without running the program from the command line, and its return values can be checked. In other words, we can test the business logic directly.
12.7.2 What Is TDD?
TDD is short for Test-Driven Development, and it usually follows these steps:
- Write a failing test, run it, and make sure it fails for the expected reason
- Write or modify just enough code to make the new test pass
- Refactor the code you just added or changed, and make sure the tests still pass
- Return to step 1 and continue
TDD is only one of many software development methods, but it can guide and support code design. Writing tests first and then writing code that passes them also helps maintain a high level of test coverage during development.
In this article, we will use TDD to implement the search logic for the program: search for a specified string in the file contents and put the matching lines into a list. This function will be called search_case_insensitive.
12.7.3 Write a Failing Test
Let’s start by naming the case-insensitive function search_case_insensitive.
First, modify the test module so that it contains a case-sensitive test and a case-insensitive test:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct 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)
);
}
}
Then write the body of search_case_insensitive:
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
- To make this function callable from outside, it must be declared public with
pub - The function needs a lifetime annotation because it has multiple non-
selfparameters, and Rust cannot determine which parameter’s lifetime should match the lifetime of the return value - The elements in the returned
Vectorare string slices taken fromcontents, so the return value should have the same lifetime ascontents; that is why both are annotated with the same lifetime'a, whilequerydoes not need a lifetime annotation - The function body only needs to compile, because the first step of TDD is to write a test that fails, so failure is the desired outcome
At this point, running the tests will definitely fail, but that is fine. This is exactly what the first TDD step is supposed to produce.
12.7.4 Write or Modify Just Enough Code for the New Test to Pass
The code for search_case_insensitive is very similar to search, so only a few small changes are needed. The logic is simple: lowercase the query and compare it against lowercase versions of the text:
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'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
}
- The
to_lowercasemethod converts a string to all lowercase - The result of
to_lowercaseis aString, so the newqueryis ownedStringrather than&str. Inside the loop, we use&querybecausecontainsdoes not acceptStringdirectly, so we pass a reference
Run the tests again:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Both tests pass.
12.7.5 Use This Function in run
Now that the function works, we can call it in run.
First, add a field to the Config struct so it can decide whether to use the normal search or the case-insensitive search_case_insensitive:
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
Modify run so it checks the configuration:
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = 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(())
}
The new constructor on Config also needs to change so it assigns case_sensitive based on the environment variable:
impl Config {
pub 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();
let case_sensitive = std::env::var("IGNORE_CASE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}
Here we use std::env::var (of course, you could also bring std::env into scope first and then use env::var). Its argument is the name of the environment variable, which conventionally is all uppercase. Here I use IGNORE_CASE, which means case-insensitive. If this environment variable is present, the search is treated as case-insensitive; if it is absent, the search is case-sensitive.
std::env::var returns a Result. If the IGNORE_CASE environment variable is set, it returns Ok(String) containing the variable’s value; otherwise it returns Err(std::env::VarError).
After std::env::var, we call is_err. If is_err sees the Err variant, it returns true, which is assigned to case_sensitive; otherwise it assigns false.
PS: honestly, writing this little program in such a rigid way is also a teaching necessity. Halfway through, I was already laughing at how much code there was. When you actually write it, there is no need to be this formal.
12.7.6 The Full Code and a Trial Run
After writing all that, here is the full code up to this point.
lib.rs:
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub 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();
let case_sensitive = std::env::var("IGNORE_CASE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = 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(())
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct 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)
);
}
}
main.rs:
use std::env;
use std::process;
use minigrep::Config;
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);
});
if let Err(e) = minigrep::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
Let’s try running it:
First, run the program without setting the environment variable and use the query to, which should match any line containing the lowercase word to:
$ cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
Now set IGNORE_CASE to 1 and keep everything else the same:
$ IGNORE_CASE=1 cargo run -- to poem.txt
You will get:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
There are no problems.
Note that if you are using powershell, you set an environment variable like this:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
This keeps the environment variable available for the entire session. If you want to remove it, write:
PS> Remove-Item Env:IGNORE_CASE
Top comments (0)