Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

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.

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-self parameters, and Rust cannot determine which parameter’s lifetime should match the lifetime of the return value
  • The elements in the returned Vector are string slices taken from contents, so the return value should have the same lifetime as contents; that is why both are annotated with the same lifetime 'a, while query does 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_lowercase method converts a string to all lowercase
  • The result of to_lowercase is a String, so the new query is owned String rather than &str. Inside the loop, we use &query because contains does not accept String directly, 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