12.7 Using Environment Variables
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
12.7.1 Review
Here is all the code written up to the previous article.
lib.rs:
#![allow(unused)]
fn main() {
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 the previous article, we used TDD to implement case-sensitive search. In this article, we will improve minigrep with an extra feature: the user can turn on case-insensitive search through an environment variable. We could expose this as a command-line option that the user must type every time, but by using an environment variable, the user can set it once and keep all searches in that terminal session case-insensitive.
12.7.2 Continuing with TDD: Case-Insensitive Search
We will again follow the TDD 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
In this article, we use TDD to add case-insensitive search. The new function will be named search_case_insensitive, and later we will control whether to use it with an environment variable.
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:
#![allow(unused)]
fn main() {
#[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:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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 (/tmp/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.15s
Running unittests src/lib.rs (target/debug/deps/minigrep-dfdfbb86b622af32)
running 2 tests
test tests::case_sensitive ... ok
test tests::case_insensitive ... 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-4c31ade9c6771135)
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:
#![allow(unused)]
fn main() {
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
}
Modify run so it checks the configuration:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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:
#![allow(unused)]
fn main() {
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 (/tmp/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
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