Working with Environment Variables - The Rust Programming Language (original) (raw)

  1. Foreword
  2. Introduction
  3. 1. Getting Started
    1. 1.1. Installation
    2. 1.2. Hello, World!
    3. 1.3. Hello, Cargo!
  4. 2. Programming a Guessing Game
  5. 3. Common Programming Concepts
    1. 3.1. Variables and Mutability
    2. 3.2. Data Types
    3. 3.3. How Functions Work
    4. 3.4. Comments
    5. 3.5. Control Flow
  6. 4. Understanding Ownership
    1. 4.1. What is Ownership?
    2. 4.2. References & Borrowing
    3. 4.3. Slices
  7. 5. Using Structs to Structure Related Data
    1. 5.1. Defining and Instantiating Structs
  8. 5.2. An Example Program Using Structs
  9. 5.3. Method Syntax
  10. 6. Enums and Pattern Matching
    1. 6.1. Defining an Enum
  11. 6.2. The match Control Flow Operator
  12. 6.3. Concise Control Flow with if let
  13. 7. Packages, Crates, and Modules
    1. 7.1. Packages and crates for making libraries and executables
  14. 7.2. Modules and use to control scope and privacy
  15. 8. Common Collections
    1. 8.1. Vectors
  16. 8.2. Strings
  17. 8.3. Hash Maps
  18. 9. Error Handling
    1. 9.1. Unrecoverable Errors with panic!
  19. 9.2. Recoverable Errors with Result
  20. 9.3. To panic! or Not To panic!
  21. 10. Generic Types, Traits, and Lifetimes
    1. 10.1. Generic Data Types
  22. 10.2. Traits: Defining Shared Behavior
  23. 10.3. Validating References with Lifetimes
  24. 11. Testing
    1. 11.1. Writing tests
  25. 11.2. Running tests
  26. 11.3. Test Organization
  27. 12. An I/O Project: Building a Command Line Program
    1. 12.1. Accepting Command Line Arguments
  28. 12.2. Reading a File
  29. 12.3. Refactoring to Improve Modularity and Error Handling
  30. 12.4. Developing the Library’s Functionality with Test Driven Development
  31. 12.5. Working with Environment Variables
  32. 12.6. Writing Error Messages to Standard Error Instead of Standard Output
  33. 13. Functional Language Features: Iterators and Closures
    1. 13.1. Closures: Anonymous Functions that Can Capture Their Environment
  34. 13.2. Processing a Series of Items with Iterators
  35. 13.3. Improving Our I/O Project
  36. 13.4. Comparing Performance: Loops vs. Iterators
  37. 14. More about Cargo and Crates.io
    1. 14.1. Customizing Builds with Release Profiles
  38. 14.2. Publishing a Crate to Crates.io
  39. 14.3. Cargo Workspaces
  40. 14.4. Installing Binaries from Crates.io with cargo install
  41. 14.5. Extending Cargo with Custom Commands
  42. 15. Smart Pointers
    1. 15.1. Box Points to Data on the Heap and Has a Known Size
  43. 15.2. The Deref Trait Allows Access to the Data Through a Reference
  44. 15.3. The Drop Trait Runs Code on Cleanup
  45. 15.4. Rc, the Reference Counted Smart Pointer
  46. 15.5. RefCell and the Interior Mutability Pattern
  47. 15.6. Creating Reference Cycles and Leaking Memory is Safe
  48. 16. Fearless Concurrency
    1. 16.1. Threads
  49. 16.2. Message Passing
  50. 16.3. Shared State
  51. 16.4. Extensible Concurrency: Sync and Send
  52. 17. Object Oriented Programming Features of Rust
    1. 17.1. Characteristics of Object-Oriented Languages
  53. 17.2. Using Trait Objects that Allow for Values of Different Types
  54. 17.3. Implementing an Object-Oriented Design Pattern
  55. 18. Patterns Match the Structure of Values
    1. 18.1. All the Places Patterns May be Used
  56. 18.2. Refutability: Whether a Pattern Might Fail to Match
  57. 18.3. All the Pattern Syntax
  58. 19. Advanced Features
    1. 19.1. Unsafe Rust
  59. 19.2. Advanced Lifetimes
  60. 19.3. Advanced Traits
  61. 19.4. Advanced Types
  62. 19.5. Advanced Functions & Closures
  63. 19.6. Macros
  64. 20. Final Project: Building a Multithreaded Web Server
    1. 20.1. A Single Threaded Web Server
  65. 20.2. Turning our Single Threaded Server into a Multithreaded Server
  66. 20.3. Graceful Shutdown and Cleanup
  67. 21. Appendix
    1. 21.1. A - Keywords
  68. 21.2. B - Operators and Symbols
  69. 21.3. C - Derivable Traits
  70. 21.4. D - Useful Development Tools
  71. 21.5. E - Editions
  72. 21.6. F - Translations
  73. 21.7. G - How Rust is Made and “Nightly Rust”

The Rust Programming Language

Working with Environment Variables

We’ll improve minigrep by adding an extra feature: an option for case-insensitive searching that the user can turn on via an environment variable. We could make this feature a command line option and require that users enter it each time they want it to apply, but instead we’ll use an environment variable. Doing so allows our users to set the environment variable once and have all their searches be case insensitive in that terminal session.

Writing a Failing Test for the Case-Insensitive search Function

We want to add a new search_case_insensitive function that we’ll call when the environment variable is on. We’ll continue to follow the TDD process, so the first step is again to write a failing test. We’ll add a new test for the new search_case_insensitive function and rename our old test fromone_result to case_sensitive to clarify the differences between the two tests, as shown in Listing 12-20:

Filename: src/lib.rs


# #![allow(unused_variables)]
#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)
        );
    }
}
#}

Listing 12-20: Adding a new failing test for the case-insensitive function we’re about to add

Note that we’ve edited the old test’s contents too. We’ve added a new line with the text "Duct tape." using a capital D that shouldn’t match the query “duct” when we’re searching in a case-sensitive manner. Changing the old test in this way helps ensure that we don’t accidentally break the case-sensitive search functionality that we’ve already implemented. This test should pass now and should continue to pass as we work on the case-insensitive search.

The new test for the case-insensitive search uses "rUsT" as its query. In the search_case_insensitive function we’re about to add, the query "rUsT"should match the line containing "Rust:" with a capital R and match the line"Trust me." even though both have different casing than the query. This is our failing test, and it will fail to compile because we haven’t yet defined the search_case_insensitive function. Feel free to add a skeleton implementation that always returns an empty vector, similar to the way we did for the search function in Listing 12-16 to see the test compile and fail.

Implementing the search_case_insensitive Function

The search_case_insensitive function, shown in Listing 12-21, will be almost the same as the search function. The only difference is that we’ll lowercase the query and each line so whatever the case of the input arguments, they’ll be the same case when we check whether the line contains the query.

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}
#}

Listing 12-21: Defining the search_case_insensitivefunction to lowercase the query and the line before comparing them

First, we lowercase the query string and store it in a shadowed variable with the same name. Calling to_lowercase on the query is necessary so no matter whether the user’s query is "rust", "RUST", "Rust", or "rUsT", we’ll treat the query as if it were "rust" and be insensitive to the case.

Note that query is now a String rather than a string slice, because callingto_lowercase creates new data rather than referencing existing data. Say the query is "rUsT", as an example: that string slice doesn’t contain a lowercaseu or t for us to use, so we have to allocate a new String containing"rust". When we pass query as an argument to the contains method now, we need to add an ampersand because the signature of contains is defined to take a string slice.

Next, we add a call to to_lowercase on each line before we check whether it contains query to lowercase all characters. Now that we’ve converted lineand query to lowercase, we’ll find matches no matter what the case of the query is.

Let’s see if this implementation passes the tests:

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

Great! They passed. Now, let’s call the new search_case_insensitive function from the run function. First, we’ll add a configuration option to theConfig struct to switch between case-sensitive and case-insensitive search. Adding this field will cause compiler errors since we aren’t initializing this field anywhere yet:

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}
#}

Note that we added the case_sensitive field that holds a Boolean. Next, we need the run function to check the case_sensitive field’s value and use that to decide whether to call the search function or thesearch_case_insensitive function, as shown in Listing 12-22. Note this still won’t compile yet:

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
# use std::error::Error;
# use std::fs::{self, File};
# use std::io::prelude::*;
#
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
#      vec![]
# }
#
# fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
#      vec![]
# }
#
# pub struct Config {
#     query: String,
#     filename: String,
#     case_sensitive: bool,
# }
#
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(())
}
#}

Listing 12-22: Calling either search orsearch_case_insensitive based on the value in config.case_sensitive

Finally, we need to check for the environment variable. The functions for working with environment variables are in the env module in the standard library, so we want to bring that module into scope with a use std::env; line at the top of src/lib.rs. Then we’ll use the var method from the envmodule to check for an environment variable named CASE_INSENSITIVE, as shown in Listing 12-23:

Filename: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
use std::env;
# struct Config {
#     query: String,
#     filename: String,
#     case_sensitive: bool,
# }

// --snip--

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 = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}
#}

Listing 12-23: Checking for an environment variable namedCASE_INSENSITIVE

Here, we create a new variable case_sensitive. To set its value, we call theenv::var function and pass it the name of the CASE_INSENSITIVE environment variable. The env::var method returns a Result that will be the successfulOk variant that contains the value of the environment variable if the environment variable is set. It will return the Err variant if the environment variable is not set.

We’re using the is_err method on the Result to check whether it’s an error and therefore unset, which means it should do a case-sensitive search. If theCASE_INSENSITIVE environment variable is set to anything, is_err will return false and the program will perform a case-insensitive search. We don’t care about the value of the environment variable, just whether it’s set or unset, so we’re checking is_err rather than using unwrap, expect, or any of the other methods we’ve seen on Result.

We pass the value in the case_sensitive variable to the Config instance so the run function can read that value and decide whether to call search orsearch_case_insensitive, as we implemented in Listing 12-22.

Let’s give it a try! First, we’ll run our program without the environment variable set and with the query to, which should match any line that contains the word “to” in all lowercase:

$ cargo run to poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

Looks like that still works! Now, let’s run the program with CASE_INSENSITIVEset to 1 but with the same query to.

If you’re using PowerShell, you will need to set the environment variable and run the program in two commands rather than one:

$ $env:CASE_INSENSITIVE=1
$ cargo run to poem.txt

We should get lines that contain “to” that might have uppercase letters:

$ CASE_INSENSITIVE=1 cargo run to poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

Excellent, we also got lines containing “To”! Our minigrep program can now do case-insensitive searching controlled by an environment variable. Now you know how to manage options set using either command line arguments or environment variables.

Some programs allow arguments and environment variables for the same configuration. In those cases, the programs decide that one or the other takes precedence. For another exercise on your own, try controlling case insensitivity through either a command line argument or an environment variable. Decide whether the command line argument or the environment variable should take precedence if the program is run with one set to case sensitive and one set to case insensitive.

The std::env module contains many more useful features for dealing with environment variables: check out its documentation to see what is available.