Files
docs-rust/ch12/ch12-05-working-with-environment-variables.html
2026-06-22 21:27:36 +05:30

460 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Working with Environment Variables</title>
</head>
<body>
<h2 id="working-with-environment-variables"><a class="header" href="#working-with-environment-variables">Working with Environment Variables</a></h2>
<p>Well improve the <code>minigrep</code> binary 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 by instead making it an
environment variable, we allow our users to set the environment variable once
and have all their searches be case insensitive in that terminal session.</p>
<!-- Old headings. Do not remove or links may break. -->
<p><a id="writing-a-failing-test-for-the-case-insensitive-search-function"></a></p>
<h3 id="writing-a-failing-test-for-case-insensitive-search"><a class="header" href="#writing-a-failing-test-for-case-insensitive-search">Writing a Failing Test for Case-Insensitive Search</a></h3>
<p>We first add a new <code>search_case_insensitive</code> function to the <code>minigrep</code> library
that will be called when the environment variable has a value. Well continue
to follow the TDD process, so the first step is again to write a failing test.
Well add a new test for the new <code>search_case_insensitive</code> function and rename
our old test from <code>one_result</code> to <code>case_sensitive</code> to clarify the differences
between the two tests, as shown in Listing 12-20.</p>
<figure class="listing" id="listing-12-20">
<span class="file-name">Filename: src/lib.rs</span>
<pre><code class="language-rust ignore does_not_compile"><span class="boring">pub fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
</span><span class="boring"> let mut results = Vec::new();
</span><span class="boring">
</span><span class="boring"> for line in contents.lines() {
</span><span class="boring"> if line.contains(query) {
</span><span class="boring"> results.push(line);
</span><span class="boring"> }
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> results
</span><span class="boring">}
</span><span class="boring">
</span>#[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)
);
}
}</code></pre>
<figcaption><a href="#listing-12-20">Listing 12-20</a>: Adding a new failing test for the case-insensitive function were about to add</figcaption>
</figure>
<p>Note that weve edited the old tests <code>contents</code> too. Weve added a new line
with the text <code>"Duct tape."</code> using a capital <em>D</em> that shouldnt match the query
<code>"duct"</code> when were searching in a case-sensitive manner. Changing the old test
in this way helps ensure that we dont accidentally break the case-sensitive
search functionality that weve already implemented. This test should pass now
and should continue to pass as we work on the case-insensitive search.</p>
<p>The new test for the case-<em>insensitive</em> search uses <code>"rUsT"</code> as its query. In
the <code>search_case_insensitive</code> function were about to add, the query <code>"rUsT"</code>
should match the line containing <code>"Rust:"</code> with a capital <em>R</em> and match the
line <code>"Trust me."</code> even though both have different casing from the query. This
is our failing test, and it will fail to compile because we havent yet defined
the <code>search_case_insensitive</code> function. Feel free to add a skeleton
implementation that always returns an empty vector, similar to the way we did
for the <code>search</code> function in Listing 12-16 to see the test compile and fail.</p>
<h3 id="implementing-the-search_case_insensitive-function"><a class="header" href="#implementing-the-search_case_insensitive-function">Implementing the <code>search_case_insensitive</code> Function</a></h3>
<p>The <code>search_case_insensitive</code> function, shown in Listing 12-21, will be almost
the same as the <code>search</code> function. The only difference is that well lowercase
the <code>query</code> and each <code>line</code> so that whatever the case of the input arguments,
theyll be the same case when we check whether the line contains the query.</p>
<figure class="listing" id="listing-12-21">
<span class="file-name">Filename: src/lib.rs</span>
<pre><code class="language-rust noplayground"><span class="boring">pub fn search&lt;'a&gt;(query: &amp;str, contents: &amp;'a str) -&gt; Vec&lt;&amp;'a str&gt; {
</span><span class="boring"> let mut results = Vec::new();
</span><span class="boring">
</span><span class="boring"> for line in contents.lines() {
</span><span class="boring"> if line.contains(query) {
</span><span class="boring"> results.push(line);
</span><span class="boring"> }
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> results
</span><span class="boring">}
</span><span class="boring">
</span>pub fn search_case_insensitive&lt;'a&gt;(
query: &amp;str,
contents: &amp;'a str,
) -&gt; Vec&lt;&amp;'a str&gt; {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&amp;query) {
results.push(line);
}
}
results
}
<span class="boring">
</span><span class="boring">#[cfg(test)]
</span><span class="boring">mod tests {
</span><span class="boring"> use super::*;
</span><span class="boring">
</span><span class="boring"> #[test]
</span><span class="boring"> fn case_sensitive() {
</span><span class="boring"> let query = "duct";
</span><span class="boring"> let contents = "\
</span><span class="boring">Rust:
</span><span class="boring">safe, fast, productive.
</span><span class="boring">Pick three.
</span><span class="boring">Duct tape.";
</span><span class="boring">
</span><span class="boring"> assert_eq!(vec!["safe, fast, productive."], search(query, contents));
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> #[test]
</span><span class="boring"> fn case_insensitive() {
</span><span class="boring"> let query = "rUsT";
</span><span class="boring"> let contents = "\
</span><span class="boring">Rust:
</span><span class="boring">safe, fast, productive.
</span><span class="boring">Pick three.
</span><span class="boring">Trust me.";
</span><span class="boring">
</span><span class="boring"> assert_eq!(
</span><span class="boring"> vec!["Rust:", "Trust me."],
</span><span class="boring"> search_case_insensitive(query, contents)
</span><span class="boring"> );
</span><span class="boring"> }
</span><span class="boring">}</span></code></pre>
<figcaption><a href="#listing-12-21">Listing 12-21</a>: Defining the <code>search_case_insensitive</code> function to lowercase the query and the line before comparing them</figcaption>
</figure>
<p>First, we lowercase the <code>query</code> string and store it in a new variable with the
same name, shadowing the original <code>query</code>. Calling <code>to_lowercase</code> on the query
is necessary so that no matter whether the users query is <code>"rust"</code>, <code>"RUST"</code>,
<code>"Rust"</code>, or <code>"rUsT"</code>, well treat the query as if it were <code>"rust"</code> and be
insensitive to the case. While <code>to_lowercase</code> will handle basic Unicode, it
wont be 100 percent accurate. If we were writing a real application, wed want
to do a bit more work here, but this section is about environment variables,
not Unicode, so well leave it at that here.</p>
<p>Note that <code>query</code> is now a <code>String</code> rather than a string slice because calling
<code>to_lowercase</code> creates new data rather than referencing existing data. Say the
query is <code>"rUsT"</code>, as an example: That string slice doesnt contain a lowercase
<code>u</code> or <code>t</code> for us to use, so we have to allocate a new <code>String</code> containing
<code>"rust"</code>. When we pass <code>query</code> as an argument to the <code>contains</code> method now, we
need to add an ampersand because the signature of <code>contains</code> is defined to take
a string slice.</p>
<p>Next, we add a call to <code>to_lowercase</code> on each <code>line</code> to lowercase all
characters. Now that weve converted <code>line</code> and <code>query</code> to lowercase, well
find matches no matter what the case of the query is.</p>
<p>Lets see if this implementation passes the tests:</p>
<pre><code class="language-console">$ 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
</code></pre>
<p>Great! They passed. Now lets call the new <code>search_case_insensitive</code> function
from the <code>run</code> function. First, well add a configuration option to the <code>Config</code>
struct to switch between case-sensitive and case-insensitive search. Adding
this field will cause compiler errors because we arent initializing this field
anywhere yet:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust ignore does_not_compile"><span class="boring">use std::env;
</span><span class="boring">use std::error::Error;
</span><span class="boring">use std::fs;
</span><span class="boring">use std::process;
</span><span class="boring">
</span><span class="boring">use minigrep::{search, search_case_insensitive};
</span><span class="boring">
</span><span class="boring">// --snip--
</span><span class="boring">
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let args: Vec&lt;String&gt; = env::args().collect();
</span><span class="boring">
</span><span class="boring"> let config = Config::build(&amp;args).unwrap_or_else(|err| {
</span><span class="boring"> println!("Problem parsing arguments: {err}");
</span><span class="boring"> process::exit(1);
</span><span class="boring"> });
</span><span class="boring">
</span><span class="boring"> if let Err(e) = run(config) {
</span><span class="boring"> println!("Application error: {e}");
</span><span class="boring"> process::exit(1);
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span>pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
<span class="boring">
</span><span class="boring">impl Config {
</span><span class="boring"> fn build(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
</span><span class="boring"> if args.len() &lt; 3 {
</span><span class="boring"> return Err("not enough arguments");
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> let query = args[1].clone();
</span><span class="boring"> let file_path = args[2].clone();
</span><span class="boring">
</span><span class="boring"> Ok(Config { query, file_path })
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn run(config: Config) -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; {
</span><span class="boring"> let contents = fs::read_to_string(config.file_path)?;
</span><span class="boring">
</span><span class="boring"> let results = if config.ignore_case {
</span><span class="boring"> search_case_insensitive(&amp;config.query, &amp;contents)
</span><span class="boring"> } else {
</span><span class="boring"> search(&amp;config.query, &amp;contents)
</span><span class="boring"> };
</span><span class="boring">
</span><span class="boring"> for line in results {
</span><span class="boring"> println!("{line}");
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> Ok(())
</span><span class="boring">}</span></code></pre>
<p>We added the <code>ignore_case</code> field that holds a Boolean. Next, we need the <code>run</code>
function to check the <code>ignore_case</code> fields value and use that to decide
whether to call the <code>search</code> function or the <code>search_case_insensitive</code>
function, as shown in Listing 12-22. This still wont compile yet.</p>
<figure class="listing" id="listing-12-22">
<span class="file-name">Filename: src/main.rs</span>
<pre><code class="language-rust ignore does_not_compile"><span class="boring">use std::env;
</span><span class="boring">use std::error::Error;
</span><span class="boring">use std::fs;
</span><span class="boring">use std::process;
</span><span class="boring">
</span>use minigrep::{search, search_case_insensitive};
// --snip--
<span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let args: Vec&lt;String&gt; = env::args().collect();
</span><span class="boring">
</span><span class="boring"> let config = Config::build(&amp;args).unwrap_or_else(|err| {
</span><span class="boring"> println!("Problem parsing arguments: {err}");
</span><span class="boring"> process::exit(1);
</span><span class="boring"> });
</span><span class="boring">
</span><span class="boring"> if let Err(e) = run(config) {
</span><span class="boring"> println!("Application error: {e}");
</span><span class="boring"> process::exit(1);
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">pub struct Config {
</span><span class="boring"> pub query: String,
</span><span class="boring"> pub file_path: String,
</span><span class="boring"> pub ignore_case: bool,
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">impl Config {
</span><span class="boring"> fn build(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
</span><span class="boring"> if args.len() &lt; 3 {
</span><span class="boring"> return Err("not enough arguments");
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> let query = args[1].clone();
</span><span class="boring"> let file_path = args[2].clone();
</span><span class="boring">
</span><span class="boring"> Ok(Config { query, file_path })
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span>fn run(config: Config) -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&amp;config.query, &amp;contents)
} else {
search(&amp;config.query, &amp;contents)
};
for line in results {
println!("{line}");
}
Ok(())
}</code></pre>
<figcaption><a href="#listing-12-22">Listing 12-22</a>: Calling either <code>search</code> or <code>search_case_insensitive</code> based on the value in <code>config.ignore_case</code></figcaption>
</figure>
<p>Finally, we need to check for the environment variable. The functions for
working with environment variables are in the <code>env</code> module in the standard
library, which is already in scope at the top of <em>src/main.rs</em>. Well use the
<code>var</code> function from the <code>env</code> module to check to see if any value has been set
for an environment variable named <code>IGNORE_CASE</code>, as shown in Listing 12-23.</p>
<figure class="listing" id="listing-12-23">
<span class="file-name">Filename: src/main.rs</span>
<pre><code class="language-rust ignore noplayground"><span class="boring">use std::env;
</span><span class="boring">use std::error::Error;
</span><span class="boring">use std::fs;
</span><span class="boring">use std::process;
</span><span class="boring">
</span><span class="boring">use minigrep::{search, search_case_insensitive};
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let args: Vec&lt;String&gt; = env::args().collect();
</span><span class="boring">
</span><span class="boring"> let config = Config::build(&amp;args).unwrap_or_else(|err| {
</span><span class="boring"> println!("Problem parsing arguments: {err}");
</span><span class="boring"> process::exit(1);
</span><span class="boring"> });
</span><span class="boring">
</span><span class="boring"> if let Err(e) = run(config) {
</span><span class="boring"> println!("Application error: {e}");
</span><span class="boring"> process::exit(1);
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">pub struct Config {
</span><span class="boring"> pub query: String,
</span><span class="boring"> pub file_path: String,
</span><span class="boring"> pub ignore_case: bool,
</span><span class="boring">}
</span><span class="boring">
</span>impl Config {
fn build(args: &amp;[String]) -&gt; Result&lt;Config, &amp;'static str&gt; {
if args.len() &lt; 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
<span class="boring">
</span><span class="boring">fn run(config: Config) -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; {
</span><span class="boring"> let contents = fs::read_to_string(config.file_path)?;
</span><span class="boring">
</span><span class="boring"> let results = if config.ignore_case {
</span><span class="boring"> search_case_insensitive(&amp;config.query, &amp;contents)
</span><span class="boring"> } else {
</span><span class="boring"> search(&amp;config.query, &amp;contents)
</span><span class="boring"> };
</span><span class="boring">
</span><span class="boring"> for line in results {
</span><span class="boring"> println!("{line}");
</span><span class="boring"> }
</span><span class="boring">
</span><span class="boring"> Ok(())
</span><span class="boring">}</span></code></pre>
<figcaption><a href="#listing-12-23">Listing 12-23</a>: Checking for any value in an environment variable named <code>IGNORE_CASE</code></figcaption>
</figure>
<p>Here, we create a new variable, <code>ignore_case</code>. To set its value, we call the
<code>env::var</code> function and pass it the name of the <code>IGNORE_CASE</code> environment
variable. The <code>env::var</code> function returns a <code>Result</code> that will be the
successful <code>Ok</code> variant that contains the value of the environment variable if
the environment variable is set to any value. It will return the <code>Err</code> variant
if the environment variable is not set.</p>
<p>Were using the <code>is_ok</code> method on the <code>Result</code> to check whether the environment
variable is set, which means the program should do a case-insensitive search.
If the <code>IGNORE_CASE</code> environment variable isnt set to anything, <code>is_ok</code> will
return <code>false</code> and the program will perform a case-sensitive search. We dont
care about the <em>value</em> of the environment variable, just whether its set or
unset, so were checking <code>is_ok</code> rather than using <code>unwrap</code>, <code>expect</code>, or any
of the other methods weve seen on <code>Result</code>.</p>
<p>We pass the value in the <code>ignore_case</code> variable to the <code>Config</code> instance so
that the <code>run</code> function can read that value and decide whether to call
<code>search_case_insensitive</code> or <code>search</code>, as we implemented in Listing 12-22.</p>
<p>Lets give it a try! First, well run our program without the environment
variable set and with the query <code>to</code>, which should match any line that contains
the word <em>to</em> in all lowercase:</p>
<pre><code class="language-console">$ 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!
</code></pre>
<p>Looks like that still works! Now lets run the program with <code>IGNORE_CASE</code> set
to <code>1</code> but with the same query <code>to</code>:</p>
<pre><code class="language-console">$ IGNORE_CASE=1 cargo run -- to poem.txt
</code></pre>
<p>If youre using PowerShell, you will need to set the environment variable and
run the program as separate commands:</p>
<pre><code class="language-console">PS&gt; $Env:IGNORE_CASE=1; cargo run -- to poem.txt
</code></pre>
<p>This will make <code>IGNORE_CASE</code> persist for the remainder of your shell session.
It can be unset with the <code>Remove-Item</code> cmdlet:</p>
<pre><code class="language-console">PS&gt; Remove-Item Env:IGNORE_CASE
</code></pre>
<p>We should get lines that contain <em>to</em> that might have uppercase letters:</p>
<!-- manual-regeneration
cd listings/ch12-an-io-project/listing-12-23
IGNORE_CASE=1 cargo run -- to poem.txt
can't extract because of the environment variable
-->
<pre><code class="language-console">Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
</code></pre>
<p>Excellent, we also got lines containing <em>To</em>! Our <code>minigrep</code> 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.</p>
<p>Some programs allow arguments <em>and</em> 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 sensitivity
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
ignore case.</p>
<p>The <code>std::env</code> module contains many more useful features for dealing with
environment variables: Check out its documentation to see what is available.</p>
</body>
</html>