mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 13:04:18 -06:00
finished ch12.5
This commit is contained in:
parent
fae65591e6
commit
17d55c746d
@ -533,7 +533,7 @@ Problem parsing arguments: not enough arguments
|
||||
|
||||
As you can see this is way more user friendly
|
||||
|
||||
### Fifth Goal: Extracting Logic form `main`
|
||||
## Fifth Goal: Extracting Logic form `main`
|
||||
Now that we finished refactoring the configuration parsing
|
||||
|
||||
Lets separate the programs logic
|
||||
@ -564,7 +564,7 @@ fn run(config: Config) {
|
||||
```
|
||||
The `run` function now contains all the remaining logic from `main` starting from reading the file.
|
||||
|
||||
#### Returning Errors from the `run` Function
|
||||
### Returning Errors from the `run` Function
|
||||
With the remaining program logic in the `run` function we can improve the error handling just like how we did with `Config::build`
|
||||
|
||||
Instead of calling `expect` the `run` function will return a `Result<T, E>` when something goes wrong
|
||||
@ -648,7 +648,7 @@ Rust tells us that our code ignored the `Result` value and the `Result` value mi
|
||||
|
||||
But we are not checking whether or not there was an error and the compiler reminds us that we probably meant to have some error-handling code here
|
||||
|
||||
#### Handling Errors Returned from `run` in main
|
||||
### Handling Errors Returned from `run` in main
|
||||
We will check for errors and handle them using a technique similar to one used in `Config::build` before but with a slight difference
|
||||
```rust
|
||||
fn main() {
|
||||
@ -729,7 +729,7 @@ And we prefix the `run` function with our crate name so that it can also be used
|
||||
|
||||
This work sets up for success in the future.
|
||||
|
||||
### Sixth Goal: Developing the Library's Functionality with Test-Driven Development
|
||||
## Sixth Goal: Developing the Library's Functionality with Test-Driven Development
|
||||
Now that the code and logic has been extracted out of *main.rs* and left behind the argument collecting and error handling
|
||||
|
||||
It is now much easier and possible to write tests for the core functionality of the code.
|
||||
@ -750,7 +750,7 @@ We will test drive the implementation of the functionality that will actually do
|
||||
|
||||
We will add this in the function called `search`
|
||||
|
||||
#### Writing a Failing Test
|
||||
### Writing a Failing Test
|
||||
First lets remove the `println!` statements because we don't need them anymore to check the program's behavior.
|
||||
|
||||
Next we'll add a `tests` module with a test function the same as [The Test Anatomy](../Writing_Tests.md) from before.
|
||||
@ -864,7 +864,7 @@ error: test failed, to rerun pass `--lib`
|
||||
```
|
||||
This test fails exactly as expected
|
||||
|
||||
#### Writing Code to Pass the Test
|
||||
### Writing Code to Pass the Test
|
||||
Our test is failing because we always return an empty vector
|
||||
|
||||
To fix this and implement `search`, the program needs to follow these steps:
|
||||
@ -874,7 +874,7 @@ To fix this and implement `search`, the program needs to follow these steps:
|
||||
4. If it doesn't do nothing
|
||||
5. Return the list of result that match
|
||||
|
||||
##### Iterating Through Lines with the lines Method
|
||||
#### Iterating Through Lines with the lines Method
|
||||
Rust includes a helpful method to handle line-by-line iterations of strings, named `lines`
|
||||
|
||||
Here it is how it would be used in this case
|
||||
@ -888,7 +888,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
The `lines` method returns an iterator.
|
||||
For now recall that when used in a `for` loop with an iterator to run some code on each item in a collection
|
||||
|
||||
##### Searching each Line for the Query
|
||||
#### Searching each Line for the Query
|
||||
Next we will check whether the current line contains our query string.
|
||||
|
||||
Strings have a helpful method named `contains` that does this for us.
|
||||
@ -911,7 +911,7 @@ At the moment we are only building up functionality
|
||||
|
||||
To get the code to compile we need to return a value from the body as we indicated in the function signature
|
||||
|
||||
##### Storing Matching Lines
|
||||
#### Storing Matching Lines
|
||||
To finish this function we need a way to store the matching lines that we want to return.
|
||||
|
||||
To do this for now we can make a mutable vector before the `for` loop and call the `push` method to store a `line` in the vector
|
||||
@ -969,7 +969,7 @@ The code in the search function isn't too bad but it doesn't take advantage of
|
||||
|
||||
This will be further improved in the iterators chapter
|
||||
|
||||
##### Using the Search Function in the `run` Function
|
||||
### Using the Search Function in the `run` Function
|
||||
Now that the `search` function is working and tested, we now need to call `search` from our `run` function.
|
||||
|
||||
We need to pass the `config.query` value and the `contents` that `run` reads from the file to search function.
|
||||
@ -1025,3 +1025,269 @@ $ cargo run -- monomorphization poem.txt
|
||||
|
||||
Now that it is finished we will finished off with a demonstration on how to work with environment variables and how to print a std error, both are useful when you are writing command line programs
|
||||
|
||||
## Seventh Bonus Goal: Working with Environment Variables
|
||||
|
||||
`minigrep` will be improved by adding an extra feature: adding an option ofr case-insensitive searching that the user can turn on by an environment variable
|
||||
|
||||
This could have been done by a command line option and require users to enter it aech time they want it to apply.
|
||||
|
||||
This is exhaustive but by making it an environment variable we allow users to set the enivronemnt variable once and have all their searches be case insensitive in that terminal session
|
||||
|
||||
### Writing a Failing Test for the Case-Insensitive `search` Function
|
||||
First we will a new `search_case_insnsitive` function that will be called when the environment varaible has a value.
|
||||
|
||||
We will contine with the TDD process, so the first step is to write another test that fails.
|
||||
|
||||
The test we add a new test is for the `search_case_insensitive` function and rename our old test from `on_result` to `case_sensitive` to clarify the differences between the two tests
|
||||
|
||||
Here is what the code should be
|
||||
```rust
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that we have editied the old test's `contents` to include a new line with the content `"Duct tape."` using a capital *D* that shouldn't match the query `"duct"` when we are searching in a case-sensitive manner.
|
||||
|
||||
Chnaing the old test ensures that we dont accidentally break the case sensitive earch functionality that has already been implemented.
|
||||
|
||||
This test shuld pass now and should continue to pass as we implement th case-insensitive search.
|
||||
|
||||
The new test uses `"rUsT"` as the query. The `search_case_insensitive` function should match the query `"rUsT"` to the line containing `"Rust:"` with a capital `R` and match the line `"Trust me."` even though both have dfferent casing from the query.
|
||||
|
||||
You should add a skeleton of `search_case_insensitive` so that the test can compile in a similar way to how `search` was done
|
||||
```rust
|
||||
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
vec![]
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing the `search_case_insensitive` Function
|
||||
Here `search_case_insensitive` function.
|
||||
It is almost the same as the `search` function. The only difference is that we lowercase the `query` and each `line` so that whatever the case of the input arguments they will always be the same case when we check whether the line contains the query
|
||||
```rust
|
||||
pub 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
|
||||
}
|
||||
```
|
||||
|
||||
First we lowercase the `query` string nad store it in a shadowed variable with the same name.
|
||||
|
||||
Calling `to_lowercase` on the query is neccessary so that the users query is lower case no matter what the user inputs.
|
||||
|
||||
While `to_lowercase` will handle basic Unicode it won't be 100% accurate.
|
||||
|
||||
If we were writing for a real application we would want to do a bit more work to handle the exceptional unicode.
|
||||
|
||||
But our goal is to work/learn about environment variables and not Unicode so this is good enough.
|
||||
|
||||
Note that `query` is now a `String` rather than a string slice because `to_lowercase` creates new data rather than referencing exisiting data.
|
||||
|
||||
Say the query is `"rUsT"` as an example: that string slice doesn't contain a lowercase `u` or `t` for us to use so we have to allocate a new `String` containing `"rust"`. When `query` is passed 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` to lowercase all characters.
|
||||
|
||||
Now that the `line` and `query` has been converted to lowercase. Now we will find matches no matter what case of the query is
|
||||
|
||||
Lets test ot see if this implementation passes the tests
|
||||
```
|
||||
$ 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
|
||||
```
|
||||
|
||||
These pass
|
||||
|
||||
Now lets call the `search_case_insensitive` function from the `run` function
|
||||
|
||||
First we will add a configuration option to the `Config` struct to switch between case-sensitive and case-insensitive search.
|
||||
|
||||
Adding this option will cause a compiler error becuase the field isnt initialized anwhere yet
|
||||
|
||||
Here is the updated version of `Config`
|
||||
```rust
|
||||
pub struct Config {
|
||||
pub query: String,
|
||||
pub file_path: String,
|
||||
pub ignore_case: bool,
|
||||
}
|
||||
```
|
||||
Notice we added the field `ignore_case` field that holds a Boolean
|
||||
|
||||
Next we need the `run` function to check the `ignore_case` field's value and use that to decide whether to cal the `search` funcion or the `search_case_insensitive` function
|
||||
|
||||
Here is the updated `run` function
|
||||
```rust
|
||||
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
let contents = fs::read_to_string(config.file_path)?;
|
||||
|
||||
let results = if config.ignore_case {
|
||||
search_case_insensitive(&config.query, &contents)
|
||||
} else {
|
||||
search(&config.query, &contents)
|
||||
};
|
||||
|
||||
for line in results {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Finally we need to check for the environment variable. The functions for working with environment variables are in the `env` module in the std library so we will bring that into scope in the *src/lib.rs*.
|
||||
|
||||
We will then use the `var` function from the `env` module to check to see if any value has been set for an environment varialbe named `IGNORE_CASE`
|
||||
|
||||
Here is how to update the `build` method
|
||||
```rust
|
||||
use std::env;
|
||||
// --snip--
|
||||
|
||||
impl Config {
|
||||
pub fn build(args: &[String]) -> Result<Config, &'static str> {
|
||||
if args.len() < 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here we create the variable `ignore_case`.
|
||||
|
||||
To set its value we call the `env::var` function and pass it the name of the `IGNORE_CASE` environment variable.
|
||||
|
||||
The `env::var` function returns a `Result` that will be successful `Ok` variant that contains the value of the environemnt varaible if the environment varaible is set to any value.
|
||||
|
||||
It will reutrn an `Err` variant if the environemnt variable is not set.
|
||||
|
||||
We use the `is_ok` method on the `Result` to check whether the environment vairable is set which means the program should do a case-insenitive search.
|
||||
|
||||
If the `IGNORE_CASE` env varaible isn't set to anthing the `is_ok` method will reutrn `false` and the program will perform a case-snesitive search.
|
||||
|
||||
We don't care about the *value* of the environment variable just whether it is set or unset.
|
||||
|
||||
Checking with `is_ok` rather than using `unwrap`, `expect` or any of the other methods that `Result` has is more appropriate.
|
||||
|
||||
We then pass the value in the `ignore_case` variale to the `Config` instance so the `run` function can read that value and decide whether to call `search_case_insensitive` or `search`
|
||||
|
||||
Now lets give it a try, first without the environment varaible 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` profile [unoptimized + debuginfo] target(s) in 0.0s
|
||||
Running `target/debug/minigrep to poem.txt`
|
||||
Are you nobody, too?
|
||||
How dreary to be somebody!
|
||||
```
|
||||
That works
|
||||
|
||||
Now lets run the program with the `IGNORE_CASE` set to `1` but with the same query `to`
|
||||
```
|
||||
$ IGNORE_CASE=1 cargo run -- to poem.txt
|
||||
```
|
||||
Here is how to set it up without needing to specify it every time
|
||||
```
|
||||
$ export IGNORE_CASE=1
|
||||
$ unset IGNORE_CASE
|
||||
```
|
||||
|
||||
Here is how to set it to PowerShell, you will need to set the environment varaible and the run the program as separate commands
|
||||
```
|
||||
$ IGNORE_CASE=1 cargo run -- to poem.txt
|
||||
```
|
||||
This will make `IGNORE_CASE` persist for the remainder of your shell session. It can be unset with the `Remove-Item` cmdlet:
|
||||
```
|
||||
PS> Remove-Item Env:IGNORE_CASE
|
||||
```
|
||||
We should get lines that contain *to* that might have uppercase letters
|
||||
```
|
||||
Are you nobody, too?
|
||||
How dreary to be somebody!
|
||||
To tell your name the livelong day
|
||||
To an admiring bog!
|
||||
```
|
||||
|
||||
The `minigrep` program can now do case-insensitive searching controlled by an environemnt variable
|
||||
|
||||
Now you know how to manage options set using either command line args or environment varaibles
|
||||
|
||||
Some programs allow for arguments *and* environment variables for the same config
|
||||
|
||||
In those cases, the programs decide that one or the other takes precedence
|
||||
|
||||
Another exercise try controlling case sensitivity through either a command line arg or an environment variable.
|
||||
|
||||
Decide whether the command line arg or the env varaible shold take precedence if the program is run with one set to case sensitive and one set to ignore case
|
||||
|
||||
The `std::env` module contains many more useful features for dealing with env variables, see its docs to see what is available.
|
||||
|
@ -1,10 +1,12 @@
|
||||
use std::fs;
|
||||
use std::error::Error;
|
||||
use std::env;
|
||||
|
||||
// refactor 9
|
||||
pub struct Config {
|
||||
pub query: String,
|
||||
pub file_path: String,
|
||||
pub ignore_case: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -16,7 +18,9 @@ impl Config {
|
||||
let query = args[1].clone();
|
||||
let file_path = args[2].clone();
|
||||
|
||||
Ok(Config { query, file_path })
|
||||
let ignore_case = env::var("IGNORE_CASE").is_ok();
|
||||
|
||||
Ok(Config { query, file_path, ignore_case })
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,8 +29,19 @@ pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// refactor 10
|
||||
// println!("With text:\n{contents}")
|
||||
// refactor 12
|
||||
let results = if config.ignore_case {
|
||||
search_case_insensitive(&config.query, &contents)
|
||||
} else {
|
||||
search(&config.query, &contents)
|
||||
};
|
||||
|
||||
for line in search(&config.query, &contents) {
|
||||
// refactor 12
|
||||
// for line in search(&config.query, &contents) {
|
||||
// println!("{line}");
|
||||
// }
|
||||
|
||||
for line in results {
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
@ -36,12 +51,33 @@ pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
|
||||
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
|
||||
// original that only can fail
|
||||
// vec![]
|
||||
let mut results = Vec::new();
|
||||
|
||||
for line in contents.lines() {
|
||||
if line.contains(query) {
|
||||
// do something with line
|
||||
results.push(line);
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
pub fn search_case_insensitive<'a>(
|
||||
query: &str,
|
||||
contents: &'a str
|
||||
) -> Vec<&'a str> {
|
||||
// orignal that only fails
|
||||
// vec![]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -49,13 +85,28 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn one_result() {
|
||||
fn case_sensitive() {
|
||||
let query = "duct";
|
||||
let contents = "\
|
||||
Rust:
|
||||
safe, fast, productive.
|
||||
Pick three.";
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,11 @@ fn main() {
|
||||
// let config = Config::new(&args);
|
||||
|
||||
// recfactor 6
|
||||
let config = Config::build(&args);
|
||||
// refactor 8
|
||||
//.unwrap_or_else(|err| {
|
||||
// println!("Problem parsing arguments: {err}");
|
||||
// process::exit(1);
|
||||
// });
|
||||
let config = Config::build(&args).unwrap_or_else(|err| {
|
||||
// refactor 8
|
||||
println!("Problem parsing arguments: {err}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
// refactor 10
|
||||
// println!("Searching for {}", config.query);
|
||||
|
Loading…
x
Reference in New Issue
Block a user