diff --git a/second-edition/nostarch/chapter12.md b/second-edition/nostarch/chapter12.md index 349582bfa7..9a65fac351 100644 --- a/second-edition/nostarch/chapter12.md +++ b/second-edition/nostarch/chapter12.md @@ -3,42 +3,46 @@ # An I/O Project -We've learned a lot over the last few chapters. Let's take that new knowledge -and apply it by building a project together. Along the way, we'll learn a bit -more about Rust's standard library. - -So what should we build? One that uses Rust's strengths. A great use of Rust is -for command line tools: Rust's speed, safety, 'single binary' output, and -cross-platform support make it a good language choice for this kind of task. So -we'll make our own version of a classic command line tool: `grep`. `grep` is -short for "Globally search a Regular Expression and Print." In the -simplest use case, it does this: - -- Takes a filename and a string as arguments. -- Reads the file. -- Finds lines in the file that contain the string argument. -- Prints out those lines. - -In addition, we'll add one extra feature: an environment variable that will -allow us to search for the string argument in a case-insensitive way. - -There's another great reason to use `grep` as an example project: a very -fully-featured version of `grep` has already been created in Rust by a -community member, Andrew Gallant. It's called `ripgrep`, and it's very, -very fast. While our version of `grep` will be fairly simple, you'll have -some of the background knowledge to understand that project if you want to see -something more real-world. - -This project will bring together a number of things we learned previously: - -- Organize code (using what we learned in modules, Chapter 7) -- Use vectors and strings (collections, Chapter 8) -- Handle errors (Chapter 9) -- Use traits and lifetimes where appropriate (Chapter 10) -- Have tests (Chapter 11) - -Additionally, we'll briefly introduce closures, iterators, and trait objects, -which Chapters XX, YY, and ZZ respectively are about to cover in detail. + + +This chapter is both a recap of the many skills you've learned so far and an +exploration of a few more standard library features. We're going to build an +input/output project to practice some of the Rust you now have under your belt. + +Rust's speed, safety, 'single binary' output, and cross-platform support make +it a good language for creating command line tools, so for our project we'll +make our own version of the classic command line tool `grep`, an acronym for +"Globally search a Regular Expression and Print." In the simplest use case, +`grep` searches a specified file for a specified string using the following +steps: + +- Take as arguments a filename and a string. +- Read the file. +- Find lines in the file that contain the string argument. +- Print out those lines. + +We'll also add one extra feature to our function that `grep` doesn't have: an +environment variable that will allow us to search for the string argument in a +case-insensitive way. + +One Rust community member, Andrew Gallant, has already created a +fully-featured, very fast version of `grep`, called `ripgrep`. By comparison, +our version of `grep` will be fairly simple, but this is a good real-world +example `grep` to use for reference or ideas. + +This project will bring together a number of concepts you've learned so far: + +- Organizing code (using what we learned in modules, Chapter 7) +- Using vectors and strings (collections, Chapter 8) +- Handling errors (Chapter 9) +- Using traits and lifetimes where appropriate (Chapter 10) +- Running tests (Chapter 11) + +We'll also briefly introduce closures, iterators, and trait objects, which +Chapters XX, YY, and ZZ respectively will cover in detail. Let's create a new project with, as always, `cargo new`: @@ -48,30 +52,45 @@ $ cargo new --bin greprs $ cd greprs ``` -We're calling our version of `grep` 'greprs', so that we don't confuse any of -our users into thinking that it's the more fully-featured version of `grep` -they may already have installed on their system. +We're calling our version 'greprs'. + + ## Accepting Command Line Arguments -Our first task is to have `greprs` accept its two command line arguments. There -are some existing libraries on crates.io that can help us do this, but since -we're learning, we'll implement this ourselves. +Our first task is to make `greprs` able to accept its two command line +arguments: the filename and a string to search for. There are some existing +libraries on crates.io that can help us do this, but since you're learning +let's implement this ourselves. + + + +### Creating the Argument Placeholders + + We'll need to call a function provided in Rust's standard library: `std::env::args`. This function returns an *iterator* of the command line -arguments that were given to our program. We haven't discussed iterators yet; -Chapter 16 will cover them fully. For our purposes, though, we don't need to -understand much about how they work in order to use them. We only need to -understand two things: +arguments that were given to our program. We haven't discussed iterators yet, +and we'll cover them fully in Chapter 16, but for our purposes now we only need +to know two things about iterators: 1. Iterators produce a series of values. 2. We can call the `collect` function on an iterator to turn it into a vector containing all of the elements the iterator produces. -Let's give it a try as shown in Listing 12-1: +Let's give it a try; use the code in Listing 12-1 to create the two command +line arguments our `grep` function needs and collect them into a vector. + + -
Filename: src/main.rs ```rust @@ -83,28 +102,27 @@ fn main() { } ``` -
- -Listing 12-1: Collect the command line arguments into a vector and print them out - -
-
+Listing 12-1: Collect the command line arguments into a vector and print them +out -First, we have a `use` statement to bring the `std::env` module into scope. -When using a function that's nested in more than one level of module, like -`std::env::args` is, it's conventional to use `use` to bring the parent module -into scope, rather than the function itself. `env::args` is less ambiguous than -a lone `args`. Also, if we end up using more than one function in `std::env`, -we only need a single `use`. +First, we bring the `std::env` module into scope with a `use` statement so that +we can use its `args` function. Notice we have two environments here: the +`std::env::args` function is nested in two levels of module. In cases where the +desired function is nested in multiple evironments it's conventional to bring +the parent module into scope, rather than the function itself, as that allows +you to easily use other functions from `std::env`, and is less ambiguous than +entering a lone `args`. + + On the first line of `main`, we call `env::args`, and immediately use `collect` -to create a vector out of it. We're also explicitly annotating the type of -`args` here: `collect` can be used to create many kinds of collections. Rust -won't be able to infer what kind of type we want, so the annotation is -required. We very rarely need to annotate types in Rust, but `collect` is one -function where you often need to. +make it into a vector. The `collect` function can be used to create many kinds +of collections so we explictly annotate the type of `args` to specify that we +want a string type. Though we very rarely need to annotate types in Rust, +`collect` is one function you do often need to annotate because Rust isn't able +to infer what kind of type you want. Finally, we print out the vector with the debug formatter, `:?`. Let's try running our code with no arguments, and then with two arguments: @@ -118,14 +136,25 @@ $ cargo run needle haystack ["target/debug/greprs", "needle", "haystack"] ``` -You'll notice one interesting thing: the name of the binary is the first -argument. The reasons for this are out of the scope of this chapter, but it's -something we'll have to remember to account for. + -Now that we have a way to access all of the arguments, let's find the ones we -care about and save them in variables as shown in Listing 12-2: +You may notice that the first argument, 0, is taken up by the name of the +binary. We'll have to account for that when setting the other arguments and +begin them at `1`. The reasons for this are out of the scope of this chapter, +but it's something to remember. + +### Setting the Arguments + +Now that we have a way to access arguments, let's set the two we need for +`grep` and save them in variables as shown in Listing 12-2: + + -
Filename: src/main.rs ```rust @@ -142,20 +171,17 @@ fn main() { } ``` -
- Listing 12-2: Create variables to hold the search argument and filename argument -
-
- -Remember, the program's name is the first argument, so we don't need `args[0]`. -We've decided that the first argument will be the string we're searching for, -so we put a reference to the first argument in the variable `search`. The -second argument will be the filename, so we put a reference to the second -argument in the variable `filename`. Let's try running this program again: +Remember, the program's name takes up the first argument at `args[0]`, so we +start at `[1]`. The first argument `greprs` will take is the string we're +searching for, so we put a reference to the first argument in the variable +`search`. The second argument will be the filename, so we put a reference to +the second argument in the variable `filename`. We add descriptions to print to +the screen to let the user know what the program is doing. Let's try running +this program again with the arguments `test` and `sample.txt`: ```text $ cargo run test sample.txt @@ -165,7 +191,14 @@ Searching for test In file sample.txt ``` -Great! There's one problem, though. Let's try giving it no arguments: + + +Great, it's working! Later we'll add some error handling to deal with +situations such as when the user provides no argmuents, but so now we'll add +file reading capabilities. + + + + ## Reading a File -Now that we have some variables containing the information that we need, let's -try using them. The next step is to open the file that we want to search. To do -that, we need a file. Create one called `poem.txt` at the root level of your -project, and fill it up with some Emily Dickinson: +Next we need to give our program the ability to open the file we specify in +order to search it. First, we need a sample file to test it with---the best +sample to use in this case is one with a small amount of text, over multiple +lines, and with some repeition. We've given you a sample poem to use in Listing +12-X. Create a file called `poem.txt` at the root level of your project, and +fill it up with some Emily Dickinson. Filename: poem.txt @@ -202,16 +240,17 @@ How public, like a frog To tell your name the livelong day To an admiring bog! ``` +Listing 12-X + -With that in place, let's edit *src/main.rs* and add code to open the file as -shown in Listing 12-3: +With that in place, edit *src/main.rs* and add code to open the file as shown +in Listing 12-3: -
Filename: src/main.rs ```rust @@ -237,35 +276,32 @@ fn main() { } ``` -
- -Listing 12-3: Read the contents of the file specified by the second argument - -
-
+Listing 12-3: Reading the contents of the file specified by the second argument -We've added a few things. First of all, we need some more `use` statements to -bring in the relevant parts of the standard library: we need `std::fs::File` -for dealing with files, and `std::io::prelude::*` contains various traits that -are useful when doing I/O, including file I/O. In the same way that Rust has a -general prelude that brings certain things into scope automatically, the -`std::io` module has its own prelude of common things you'll need when working -with I/O. Unlike the default prelude, we must explicitly `use` the prelude in -`std::io`. - -In `main`, we've added three things: first, we get a handle to the file and -open it by using the `File::open` function and passing it the name of the file -specified in the second argument. Second, we create a mutable, empty `String` -in the variable `contents`, then call `read_to_string` on our file handle with -our `contents` string as the argument; `contents` is where `read_to_string` -will place the data it reads. Finally, we print out the entire file contents, -which is a way for us to be sure our program is working so far. - -Let's try running this code, specifying any string for the first argument (since -we haven't implemented the searching part yet) and our *poem.txt* file as the -second argument: +First, we add some more `use` statements to bring in relevant parts of the +standard library: we need `std::fs::File` for dealing with files, and +`std::io::prelude::*` contains various traits that are useful when doing I/O, +including file I/O. In the same way that Rust has a general prelude that brings +certain things into scope automatically, the `std::io` module has its own +prelude of common things you'll need when working with I/O. Unlike the default +prelude, we must explicitly `use` the prelude in `std::io`. + +In `main`, we've added three things: first, we set a mutable handle to the file +and add functionality to open it using the `File::open` function, and give it +the filename argument so that it will open whatever we pass as the second +argument. Second, we create a variable to hold the data the program reads in +called `contents`, which we make a mutable, empty `String`. This will hold the +content of the file given as the second argument. Then we call `read_to_string` +on our file handle with our `contents` string as the argument. + +Finally, we print out the contents of `contents` so we can check our program is +working so far. + +Let's try running this code with any string passed for the first argument +(since we haven't implemented the searching part yet) and our *poem.txt* file +as the second argument: ```text $ cargo run the poem.txt @@ -285,71 +321,89 @@ To tell your name the livelong day To an admiring bog! ``` -Great! Our code is working. However, it's got a few flaws. Because our program -is still small, these flaws aren't a huge deal, but as our program grows, it -will be harder and harder to fix them in a clean way. Let's do the refactoring -now, instead of waiting. The refactoring will be much easier to do with only -this small amount of code. +Great! Our code read in and printed out the content of the file. There are +still a few flaws: + + + +While our program is still small, these flaws aren't a big problem, but as our +program grows, it will be harder to fix them cleanly. It's good practice to +begin refactoring early on when developing a program, as it's much easier to do +with only this small amount of code, so we'll do that now. -## Improving Error Handling and Modularity +## Refactoring to Improve Error Handling and Modularity There are four problems that we'd like to fix to improve our program, and they -all have to do with potential errors and the way the program is structured. The -first problem is where we open the file: we've used `expect` to print out an -error message if opening the file fails, but the error message only says "file -not found". There are a number of ways that opening a file can fail, but we're -always assuming that it's due to the file being missing. For example, the file -could exist, but we might not have permission to open it: right now, we print -an error message that says the wrong thing! - -Secondly, our use of `expect` over and over is similar to the earlier issue we -noted with the `panic!` on indexing if we don't pass any command line -arguments: while it _works_, it's a bit unprincipled, and we're doing it all -throughout our program. It would be nice to put our error handling in one spot. - -The third problem is that our `main` function now does two things: it parses -arguments, and it opens up files. For such a small function, this isn't a huge -problem. However, as we keep growing our program inside of `main`, the number of -separate tasks in the `main` function will get larger and larger. As one -function gains many responsibilities, it gets harder to reason about, harder to -test, and harder to change without breaking one of its parts. +all have to do with potential errors and the way the program is structured. + +The first problem is that we've used `expect` to print out an error message if +opening the file fails, but our blanket error message only says "file not +found". There are a number of ways that opening a file can fail besides a +missing file, for example, the file might exist, but we might not have +permission to open it: right now, we print a blanket error message that may +give the user the wrong advice! + +Secondly, we use `expect` repeatedly to deal with different errors, which +result in index errors when no arguments are provided in the command line. +While it _works_, it's a bit unprincipled, and we're doing it all throughout +our program. It would be much more user friendly to put our error handling in +one spot. + + + +Thirdly, our `main` function now performs two tasks: it parses arguments, and +opens up files. For such a small function, this isn't a huge problem. However, +if we keep growing our program inside of `main` alone, the number of separate +tasks the `main` function handles will grow, and as a function gains +responsibilities it gets harder to reason about, harder to test, and harder to +change without breaking one of its parts. It's better to separate out +functionality. This also ties into our fourth problem: while `search` and `filename` are configuration variables to our program, variables like `f` and `contents` are used to perform our program's logic. The longer `main` gets, the more variables -we're going to bring into scope, and the more variables we have in scope, the -harder it is to keep track of which ones we need for which purpose. It would be -better if we grouped the configuration variables into one structure to make -their purpose clear. +we're going to need to bring into scope; the more variables we have in scope, +the harder it is to keep track of the purpose of each. It's better to group the +configuration variables into one structure to make their purpose clear. Let's address these problems by restructuring our project. + + ### Separation of Concerns for Binary Projects -These kinds of organizational problems are common to many similar kinds of -projects, so the Rust community has developed a pattern for organizing the -separate concerns. This pattern is useful for organizing any binary project -you'll build in Rust, so we can justify doing this refactoring a bit earlier, -since we know that our project fits the pattern. The pattern looks like this: +Organizational problems are common to many similar kinds of projects, so the +Rust community has developed a kind of guideline pattern for organizing the +separate concerns of a program. The pattern looks like this: 1. Split your program into both a *main.rs* and a *lib.rs*. 2. Place your command line parsing logic into *main.rs*. 3. Place your program's logic into *lib.rs*. -4. The job of the `main` function is: +4. The job of the `main` function should be to: * parse arguments * set up any other configuration * call a `run` function in *lib.rs* * if `run` returns an error, handle that error -Whew! The pattern sounds more complicated than it is, honestly. It's all about -separating concerns: *main.rs* handles actually running the program, and -*lib.rs* handles all of the actual logic of the task at hand. Let's re-work our -program into this pattern. First, let's extract a function whose purpose is -only to parse arguments. Listing 12-4 shows the new start of `main` that calls -a new function `parse_config`, which we're still going to define in -*src/main.rs*: + + +The pattern is simple! It's all about separating concerns: *main.rs* handles +running the program, and *lib.rs* handles all of the logic of the task at hand. +Let's re-work our program into this pattern. + + + +#### Extracting the Argument Parser + +First, we'll extract the functionality for parsing arguments. Listing 12-4 +shows the new start of `main` that calls a new function `parse_config`, which +we're still going to define in *src/main.rs*: -
Filename: src/main.rs ```rust,ignore @@ -369,38 +423,43 @@ fn parse_config(args: &[String]) -> (&str, &str) { } ``` -
- Listing 12-4: Extract a `parse_config` function from `main` -
-
- -This may seem like overkill, but we're working in small steps. After making -this change, run the program again to verify that the argument parsing still -works. It's good to check your progress often, so that you have a better idea -of which change caused a problem, should you encounter one. + + +This may seem like overkill for our small program, but we're working in small +steps. After making this change, run the program again to verify that the +argument parsing still works. It's good to check your progress often, as that +will help you identify the cause of problems when they occur. -### Grouping Configuration Values +#### Grouping Configuration Values -Now that we have a function, let's improve it. Our code still has an indication -that there's a better design possible: we return a tuple, but then immediately -break that tuple up into individual parts again. This code isn't bad on its -own, but there's one other sign we have room for improvement: we called our -function `parse_config`. The `config` part of the name is saying the two values -we return should really be bound together, since they're both part of one -configuration value. +We can improve on this function. At the moment, we return a tuple, but then +immediately break that tuple up into individual parts again, which seems a +clear indicator that we can make it more efficient. + +Another indicator that there's room for improvement is the `config` part of +`parse_config`, which implies that the two values we return should be bound +together, since they're both part of one configuration value. + + > Note: some people call this anti-pattern of using primitive values when a > complex type would be more appropriate *primitive obsession*. -Let's introduce a struct to hold all of our configuration. Listing 12-5 shows -the addition of the `Config` struct definition, the refactoring of -`parse_config`, and updates to `main`: + + +We'll introduce a struct to hold all of our configuration so that the +information is held in one place. Listing 12-5 shows the addition of the +`Config` struct definition, the refactoring of `parse_config`, as well as a few +updates to `main` we'll talk through: -
Filename: src/main.rs ```rust @@ -433,66 +492,74 @@ fn parse_config(args: &[String]) -> Config { } ``` -
- Listing 12-5: Refactoring `parse_config` to return an instance of a `Config` struct -
-
- The signature of `parse_config` now indicates that it returns a `Config` value. -In the body of `parse_config`, we used to be returning string slices that were -references to `String` values in `args`, but we've defined `Config` to contain -owned `String` values. Because the argument to `parse_config` is a slice of -`String` values, the `Config` instance can't take ownership of the `String` -values: that violates Rust's borrowing rules, since the `args` variable in -`main` owns the `String` values and is only letting the `parse_config` function -borrow them. - -There are a number of different ways we could manage the `String` data; for -now, we'll take the easy but less efficient route, and call the `clone` method -on the string slices. The call to `clone` will make a full copy of the string's -data for the `Config` instance to own, which does take more time and memory -than storing a reference to the string data, but cloning the data makes our -code very straightforward. +In the body of `parse_config`, where we used to return string slices as +references to `String` values in `args`, we've now defined `Config` to contain +owned `String` values. This is because the `args` variable in `main` owns the +`String` values and is only letting the `parse_config` function borrow them, +meaning it would violate Rust's borrowing rules to let `Config take ownership +of the `String` values, since the argument to `parse_config` is a slice of +`String` values. + + + +There are a number of different ways we could manage the `String` data, and the +easiest, though somewhat inefficient, route is to call the `clone` method on +the string slices. This will make a full copy of the string's data for the +`Config` instance to own, which does take more time and memory than storing a +reference to the string data, but also makes our code very straightforward, so +in this simple circumstance is a worthwhile trade-off. + +We've updated `main` so that it places the instance of `Config` that +`parse_config` returns into a variable named `config`, and updated the code +that previously used the separate `search` and `filename` variables so that is +now uses the fields on the `Config` struct instead. + + > #### The Tradeoffs of Using `clone` > -> There's a tendency amongst many Rustaceans to prefer not to use `clone` to fix -> ownership problems due to its runtime cost. In Chapter XX on iterators, we'll -> learn how to make this situation more efficient. For now, it's okay to copy a -> few strings to keep making progress. We're only going to be making these -> copies once, and our filename and search string are both very small. It's -> better to have a working program that's a bit inefficient than try to -> hyper-optimize code on your first pass. As you get more experienced with Rust, -> it'll be easier to skip this step, but for now, it's perfectly acceptable to -> call `clone`. +> There's a tendency amongst many Rustaceans to avoid using `clone` to fix +> ownership problems because of its runtime cost. In Chapter XX on iterators, +> you'll learn how to use more efficient methods in this kind of situation, but +> for now, it's okay to copy a few strings to keep making progress since we'll +> only make these copies once, and our filename and search string are both very +> small. It's better to have a working program that's a bit inefficient than +> try to hyper-optimize code on your first pass. As you get more experienced +> with Rust, it'll be easier to go straight to the desirable method, but for +> now it's perfectly acceptable to call `clone`. -We've updated `main` to put the instance of `Config` that `parse_config` -returns in a variable named `config`, and we've updated the code that was using -the separate `search` and `filename` variables to use the fields on the -`Config` struct instead. - ### Creating a Constructor for `Config` -Let's now think about the purpose of `parse_config`: it's a function that -creates a `Config` instance. We've already seen a convention for functions that -create instances: a `new` function, like `String::new`. Listing 12-6 shows the -result of transforming `parse_config` into a `new` function associated with our -`Config` struct: + + +The purpose of the `parse_config function is to create a `Config` instance, but +as we know there are other ways to create new instanes, such as the `new` +function, like `String::new` to create a new string. We'll transform our +`parse_config` into a `new` function associated with our `Config` struct so +that : + + -
Filename: src/main.rs ```rust - fn main() { let args: Vec = env::args().collect(); @@ -516,28 +583,31 @@ impl Config { } ``` -
- Listing 12-6: Changing `parse_config` into `Config::new` -
-
- -We've changed the name of `parse_config` to `new` and moved it within an `impl` +We've changed the name of `parse_config` to `new` and moved it within an `impl` block. We've also updated the callsite in `main`. Try compiling this again to make sure it works. -### Returning a `Result` from the Constructor + + +### Fixing the Error Handling -Here's our last refactoring of this method: remember how accessing the vector -with indices 1 and 2 panics when it contains fewer than 3 items and gives a bad -error message? Let's fix that! Listing 12-7 shows how we can check that our -slice is long enough before accessing those locations, and panic with a better -error message: +Now we'll do the last refactoring of this method, and fix our error handling: +remember that an attempt as accessing the vector with indices 1 and 2 causes +the program to panic if it contains fewer than 3 items, and currently it gives +a bad error message. We'll fix that now. + +#### Improving the Error Message + +In Listing 12-7 we change it so the program checks that the slice is long +enough before accessing those locations, and if it isn't the panic returns a +better error message: -
Filename: src/main.rs ```rust,ignore @@ -549,17 +619,14 @@ fn new(args: &[String]) -> Config { // ...snip... ``` -
- Listing 12-7: Adding a check for the number of arguments -
-
- + + With these extra few lines of code in `new`, let's try running our program -without any arguments: +without any arguments and see what error occurs: ```bash $ cargo run @@ -569,14 +636,18 @@ thread 'main' panicked at 'not enough arguments', src\main.rs:29 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -This is a bit better! We at least have a reasonable error message here. -However, we also have a bunch of extra information that we don't want to give -to our users. We can do better by changing the type signature of `new`. Right -now, it returns only a `Config`, so there's no way to indicate that an error -happened while creating our `Config`. Instead, we can return a `Result`, as -shown in Listing 12-8: +This output is better, we now have a reasonable error message. However, we also +have a bunch of extra information we don't want to give to our users. + + + +#### Returning a Result from the Constructor + +We can resolve this by changing the type signature of `new`. Right now, it +returns only a `Config`, so there's no way to indicate that an error happened +while creating our `Config`. Instead, we can return a `Result`, as shown in +Listing 12-8: -
Filename: src/main.rs ```rust,ignore @@ -597,30 +668,32 @@ impl Config { } ``` -
- Listing 12-8: Return a `Result` from `Config::new` -
-
- + + Our `new` function now returns a `Result`, with a `Config` instance in the -success case and a `&'static str` when an error happens. Recall from "The -Static Lifetime" section in Chapter 10 `&'static str` is the type of string -literals, which is what our error message is for now. +success case and a `&'static str` in the error case. Recall from "The Static +Lifetime" section in Chapter 10 that `&'static str` is the type for string +literals, which is our error message type for now. We've made two changes in the body of the `new` function: instead of calling -`panic!` if there aren't enough arguments, we now return an `Err` value. We -wrapped the `Config` return value in an `Ok`. These changes make the function -conform to its new type signature. +`panic!` when the user doesn't pass enough arguments, we now return an `Err` +value, and we've wrapped the `Config` return value in an `Ok`. These changes +make the function conform to its new type signature. + +By having `Config::new` return an `Err` value, it allows the `main` functionto +handle the `Result` value returned from the `new` function and exit the process +more cleanly. ### Calling `Config::new` and Handling Errors + + Now we need to make some changes to `main` as shown in Listing 12-9: -
Filename: src/main.rs ```rust,ignore @@ -637,39 +710,41 @@ fn main() { // ...snip... ``` -
- Listing 12-9: Exiting with an error code if creating a new `Config` fails -
-
- + We've added a new `use` line to import `process` from the standard library. -In the `main` function itself, we'll handle the `Result` value returned + + + +In this listing we're using a method we haven't covered before: +`unwrap_or_else`, which is defined on `Result` by the standard library. +Using `unwrap_or_else` allows us some custom, non-`panic!` error handling. If +the `Result` is an `Ok` value, this method's behavior is similar to `unwrap`: +it returns the inner value `Ok` is wrapping. However, if the value is an `Err` +value, this method calls a *closure*, which is an anonymous function we define +and pass as an argument to `unwrap_or_else`. We'll be covering closures in more +detail in Chapter XX; what you need to know in this case is that +`unwrap_or_else` will pass the inner value of the `Err` to our closure in the +argument `err` that appears between the vertical pipes. + + + +The error handling here is only two lines: we print out the error, then call +`std::process::exit`, which will execute the program immediately and return the +number that was passed as a return code. By convention, a zero indicates +success and any other value means failure. This isn't dissimilar to the +`panic!`-based handling we used in Listing 12-7, with the exception that we no +longer get all the extra output. Let's try it: -```bash +```text $ cargo run Compiling greprs v0.1.0 (file:///projects/greprs) Finished debug [unoptimized + debuginfo] target(s) in 0.48 secs @@ -681,12 +756,15 @@ Great! This output is much friendlier for our users. ### Handling Errors from the `run` Function -Now that we're done refactoring our configuration parsing, let's improve our -program's logic. Listing 12-10 shows the code after extracting a function named -`run` that we'll call from `main`. The `run` function contains the code that -was in `main`: +Now we're done refactoring our configuration parsing; let's improve our +program's logic. We'll extract a function named `run` that we'll call from +`main` and add to to our code, as shown in Listing 12-10. The `run` function +contains the code that was in `main`: + + -
Filename: src/main.rs ```rust,ignore @@ -711,24 +789,24 @@ fn run(config: Config) { // ...snip... ``` -
+Listing 12-10: Extracting the `run` functionality for the rest of the program +logic -Listing 12-10: Extracting a `run` functionality for the rest of the program logic + -
-
+The `run` function now holds the lines that were previously in `main`, and +takes a `Config` as an argument. - + -The contents of `run` are the previous lines that were in `main`, and the `run` -function takes a `Config` as an argument. Now that we have a separate function, -we can make a similar improvement to the one we made to `Config::new` in -Listing 12-8: let's return a `Result` instead of calling `panic!` via -`expect`. Listing 12-11 shows the addition of a `use` statement to bring -`std::error::Error` struct into scope and the changes to the `run` function -to return a `Result`: +#### Heading? + +With this function separated, we'll improve the error handling like we did with +`Config::new` in Listing 12-8: rather than allowing the program to panic, it +will return a `Result` when something goes wrong. In Listing 12-11 we add +a `use` statement to bring the `std::error::Error` struct into scope and we +change the `run` function to return a `Result`: -
Filename: src/main.rs ```rust,ignore @@ -748,38 +826,36 @@ fn run(config: Config) -> Result<(), Box> { } ``` -
- Listing 12-11: Changing the `run` function to return `Result` -
-
- -We've made three big changes here. The first is the return type of the `run` -function is now `Result<(), Box>`. Previously, our function returned the -unit type, `()`, so that's still the value returned in the `Ok` case. For our -error type, we're going to use `Box`. This is called a *trait object*, -which we'll be covering in Chapter XX. For now, think of it like this: -`Box` means the function will return some kind of type that implements -the `Error` trait, but we're not specifying what particular type the return -value will be. This gives us flexibility to return error values that may be of -different types in different error cases. `Box` is a smart pointer to heap -data, and we'll be going into detail about `Box` in Chapter YY. - -The second change is that we've removed our calls to `expect` in favor of `?`, +We've made three big changes here. First, we changed the return type of the +`run` function to `Result<(), Box>`. Our function previously returned +the unit type, `()`, and we keep that as the value returned in the `Ok` case. + + + +For our error type we use the *trait object* `Box`---we'll be covering +trait objects in Chapter XX, but for now just know that `Box` here means +the function will return a type that implements the `Error` trait, but that we +don't have to specify what particular type the return value will be. This gives +us flexibility to return error values that may be of different types in +different error cases. We'll be go into detail about `Box` in Chapter YY. + +The second change we make is to remove the calls to `expect` in favor of `?`, like we talked about in Chapter 9. Rather than `panic!` on an error, this will -return the error value from the function we're in for the caller to handle. +return the error value from the current function for the caller to handle. -The third change is that we're now returning an `Ok` value from this function -in the success case. Because we've declared the `run` function's success type -as `()` in the signature, we need to wrap the unit type value in the `Ok` -value. `Ok(())` looks a bit strange at first, but using `()` in this way is the -idiomatic way to indicate that we're calling `run` for its side effects only; -it doesn't return anything interesting. +Thirdly, this function now returns an `Ok` value in the success case. We've +declared the `run` function's success type as `()` in the signature, which +means we need to wrap the unit type value in the `Ok` value. This `Ok(())` +syntax may look a bit strange at first, but using `()` like is the idiomatic +way to indicate that we're calling `run` for its side effects only; it doesn't +return anything we actually need. -This will compile, but with a warning: +When you run this, it will compile, but with a warning: ```text warning: unused result which must be used, #[warn(unused_must_use)] on by default @@ -789,10 +865,19 @@ warning: unused result which must be used, #[warn(unused_must_use)] on by defaul | ^^^^^^^^^^^^ ``` -Rust is trying to tell us that we're ignoring our `Result`, which might be an -error value. Let's handle that now. We'll use a similar technique as the way we -handled failure with `Config::new` in Listing 12-9, but with a slight -difference: +Rust is telling us that our code ignores our `Result`, which might be an error +value. + + + +Let's rectify that now. + +#### Heading + +We'll use a similar technique to the way we handled failure with `Config::new` +in Listing 12-9, but with a slight difference: + + Filename: src/main.rs @@ -813,32 +898,35 @@ fn main() { -Instead of `unwrap_or_else`, we use `if let` to see if `run` returns an `Err` -value and call `process::exit(1)` if so. Why? The distinction between this case -and the `Config::new` case is a bit subtle. With `Config::new`, we cared about -two things: +We use `if let` to check whether `run` returns an `Err` value, rather than +`unwrap_or_else`, and call `process::exit(1)` if it does. -1. Detecting any errors that happen -2. Getting a `Config` if no errors happened + -In this case, because `run` returns a `()` in the success case, the only thing -we care about is the first case: detecting an error. If we used -`unwrap_or_else`, we'd get its return value, which would be `()`. That's not -very useful. +There are a couple of reasons we use a different method here. With +`Config::new`, we cared about two things: detecting errors and getting a +`Config` there are no errors. -The bodies of the `if let` and of the `unwrap_or_else` are the same in both -cases though: we print out an error and exit. +In this case, though, because `run` returns a `()` in the success case, we only +care about detecting an error, so we don't need `unwrap_or_else` to return its +value as it would only be `()`. + +The bodies of the `if let` and the `unwrap_or_else` functions are the same in +both cases though: we print out an error and exit. ### Split Code into a Library Crate -This is looking pretty good! There's one more thing we haven't done yet: split -the *src/main.rs* up and put some code into *src/lib.rs* Let's do that now: -move the `run` function from *src/main.rs* to a new file, *src/lib.rs*. You'll +This is looking pretty good so far! Now we need to split the *src/main.rs* file +up and put some code into *src/lib.rs* + + + +Move the `run` function from *src/main.rs* to a new file, *src/lib.rs*. You'll also need to move the relevant `use` statements and the definition of `Config` -and its `new` method as well. Your *src/lib.rs* should now look like Listing -12-12: +and its `new` method in the XXX file as well. Your *src/lib.rs* should now look +like Listing 12-12: -
Filename: src/lib.rs ```rust,ignore @@ -879,24 +967,23 @@ pub fn run(config: Config) -> Result<(), Box>{ } ``` -
- Listing 12-12: Moving `Config` and `run` into *src/lib.rs* -
-
- -Notice we also made liberal use of `pub`: on `Config`, its fields and its `new` +We've made liberal use of `pub` here, on `Config`, its fields and its `new` method, and on the `run` function. -Now in *src/main.rs*, we need to bring in the code that's now in *src/lib.rs* -through `extern crate greprs`. Then we need to add a `use greprs::Config` line -to bring `Config` into scope, and prefix the `run` function with our crate name -as shown in Listing 12-13: + + +#### Calling the Library Code + +Now we need to call the code we moved to *src/lib.rs* in our *src/main.rs*, by +using `extern crate greprs`. Then we'll add a `use greprs::Config` line to +bring `Config` into scope, and prefix the `run` function with our crate name as +shown in Listing 12-13: -
Filename: src/main.rs ```rust,ignore @@ -926,20 +1013,20 @@ fn main() { } ``` -
- Listing 12-13: Bringing the `greprs` crate into the scope of *src/main.rs* -
-
- -With that, everything should work again. Give it a few `cargo run`s and make -sure you haven't broken anything. Whew! That all was a lot of work, but we've -set ourselves up for success in the future. We've set up a way to handle errors -in a much nicer fashion, and we've made our code slightly more modular. Almost -all of our work will be done in *src/lib.rs* from here on out. +With that, all the functionality should be connected and should work. Give it a +few `cargo run`s and make sure you haven't broken anything. + + + +Whew! That was a lot of work, but we've set ourselves up for success in the +future. Now it's much easier to handle errors, and we've made our code more +modular. Almost all of our work will be done in *src/lib.rs* from here on out. Let's take advantage of this newfound modularity by doing something that would have been hard with our old code, but is easy with our new code: write some @@ -947,23 +1034,36 @@ tests! ## Testing the Library's Functionality -Writing tests for the core functionality of our code is now easier since we -extracted the logic into *src/lib.rs* and left all the argument parsing and -error handling in *src/main.rs*. We can now call our code directly with various -arguments and check return values without having to call our binary from the -command line. - -We're going to write a function named `grep` that takes our search term and the -text to search and produces a list of search results. Let's remove that -`println!` from `run` (and from *src/main.rs* as well, as we don't really need -those anymore either), and call the new `grep` function with the options we've -collected. We'll add a placeholder implementation of the function for now, and +Now we've extracted the logic into *src/lib.rs* and left all the argument +parsing and error handling in *src/main.rs*, it's much easier for us to write +tests for the core functionality of our code. + +Now we can call our code directly with various arguments and check return +values without having to call our binary from the command line. + + + +For our test we'll write a function named `grep` that takes our search term and +the text to search and produces a list of search results. + +### Testing to Fail + + + +First, since we don't really need them any more, let's remove `println!` from +`run` from both lib.rs and *src/main.rs*. Then from lib.rs we'll call the new +`grep` function with the options we've collected. + + + +We'll add a placeholder implementation of the `grep` function for now, and add a test that specifies the behavior we'd like the `grep` function to have. The -test will fail with our placeholder implementation, of course, but we can make -sure the code compiles and that we get the failure message we expect. Listing -12-14 shows these modifications: +test will fail with our placeholder implementation, of course, but with this we +can make sure the code compiles and that we get the failure message we expect. +Listing 12-14 shows these modifications: -
Filename: src/lib.rs ```rust @@ -1002,29 +1102,25 @@ Pick three."; } ``` -
- -Listing 12-14: Creating a function where our logic will go and a failing test +Listing 12-14: Creating a function to hold our test logic and a failing test for that function -
-
- Notice that we need an explicit lifetime `'a` declared in the signature of -`grep` and used with the `contents` argument and the return value. Remember, -lifetime parameters are used to specify which arguments' lifetimes connect to -the lifetime of the return value. In this case, we're indicating that the -vector we're returning is going to contain string slices that reference slices -of the argument `contents`, as opposed to referencing slices of the argument -`search`. Another way to think about what we're telling Rust is that the data -returned by the `grep` function will live as long as the data passed into this -function in the `contents` argument. This is important! Given that the data a -slice references needs to be valid in order for the reference to be valid, if -the compiler thought that we were making string slices of `search` rather than -`contents`, it would do its safety checking incorrectly. If we tried to compile -this function without lifetimes, we would get this error: +`grep` and used with the `contents` argument and the return value. Remember, we +use lifetime parameters to specify which argument lifetime is connected to the +lifetime of the return value. In this case, we're indicating that the returned +vector should contain string slices that reference slices of the argument +`contents` (rather than the argument `search`). + +In other words, we're telling Rust that the data returned by the `grep` +function will live as long as the data passed into the `grep` function in the +`contents` argument. This is important! The data referenced *by* a slice needs +to be valid in order for the reference to be valid; if the compiler assumed we +were making string slices of `search` rather than `contents`, it would do its +safety checking incorrectly. If we tried to compile this function without +lifetimes, we would get this error: ```text error[E0106]: missing lifetime specifier @@ -1038,16 +1134,15 @@ error[E0106]: missing lifetime specifier `contents` ``` -Rust can't possibly know which of the two arguments we need, so it needs us to -tell it. Because `contents` is the argument that contains all of our text and -we want to return the parts of that text that match, we know `contents` is the +Rust can't possibly know which of the two arguments we need, so we need to tell +it. Because `contents` is the argument that contains all of our text and we +want to return the parts of that text that match, we know `contents` is the argument that should be connected to the return value using the lifetime syntax. -Connecting arguments to return values in the signature is something that other -programming languages don't make you do, so don't worry if this still feels -strange! Knowing how to specify lifetimes gets easier over time, and practice -makes perfect. You may want to re-read the above section or go back and compare -this example with the Lifetime Syntax section in Chapter 10. +Other programming languages don't require you to connect arguments to return +values in the signature, so this may feel strange at first, but will get easier +over time. You may want to compare this example with the Lifetime Syntax +section in Chapter 10 to help you get your head around it. Now let's try running our test: @@ -1076,18 +1171,27 @@ test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured error: test failed ``` -Great, our test fails, exactly as we expected. Let's get the test to pass! It's -failing because we always return an empty vector. Here's what we're going to do -to implement `grep`: +Great, our test fails, exactly as we expected. Let's get the test to pass! + +### Testing to Succeed + + + +Currently, our test is failing because we always return an empty vector. To fix +that and implement `grep`, our program needs to follow these steps: 1. Iterate through each line of the contents. 2. Check if the line contains our search string. * If it does, add it to the list of values we're returning. - * If not, do nothing. + * If it doesn't, do nothing. 3. Return the list of results that match. -Let's take each step at a time, starting with iterating through lines. Strings -have a helpful method to handle this, conveniently named `lines`: +Let's take each step at a time, starting with iterating through lines. + +#### Iterating Through Lines with lines + +Rust has a helpful method to handle line-by-line iteration of strings, +conveniently named `lines`, that works like this: Filename: src/lib.rs @@ -1099,12 +1203,20 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` +Listing 12-X: Iterating through lines + -We're using a `for` loop along with the `lines` method to get each line in turn. -Next, let's see if our line contains the search string. Luckily, strings have a -helpful method named `contains` that does this for us! Using the `contains` -method looks like this: +We use a `for` loop along with the `lines` method to get each line in turn. + + + +#### Searching the Line for a String + +Next, we'll add functionality to check if the current line contains the search +string. Luckily, strings have another helpful method named `contains` that does +this for us! Add the `contains` method to our program so far like this: Filename: src/lib.rs @@ -1118,12 +1230,16 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` +Listing 12-X: Adding functionality to search for a string + -Finally, we need a way to store the lines that contain our search string. For -that, we can make a mutable vector before the `for` loop and call the `push` -method to store a `line` in the vector. After the `for` loop, we return the -vector: +#### Storing Matching Lines + +Finally, we need a way to store the lines that do contain our search string. +For that, we can make a mutable vector before the `for` loop and call the +`push` method to store a `line` in the vector. After the `for` loop, we return +the vector: Filename: src/lib.rs @@ -1141,9 +1257,12 @@ fn grep<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` +Listing 12-X: Storing the results + -Let's give it a try: +We push the lines with the matching content to the `line` vector. Let's run +this and give it a try: ```text $ cargo test @@ -1165,16 +1284,23 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -Great! It works. Now that our test is passing, we could consider opportunities +Our reults passed and nothing failes, great, it works! + + + + -Now that the `grep` function is working, we need to do one last thing inside of -the `run` function: we never printed out the results! We'll do that by adding -a `for` loop that prints each line returned from the `grep` function: +#### Returning Matching Lines + +With the functionality working, now we just need to print out the results. +We'll do that by adding a `for` loop that prints each line returned from the +`grep` function: Filename: src/lib.rs @@ -1195,6 +1321,8 @@ pub fn run(config: Config) -> Result<(), Box> { + + Now our whole program should be working! Let's try it out: ```text @@ -1217,22 +1345,38 @@ To tell your name the livelong day To an admiring bog! ``` + + Excellent! We've built our own version of a classic tool, and learned a lot about how to structure applications. We've also learned a bit about file input and output, lifetimes, testing, and command line parsing. + + ## Working with Environment Variables -Let's add one more feature: case insensitive searching. In addition, this -setting won't be a command line option: it'll be an environment variable -instead. We could choose to make case insensitivity a command line option, but -our users have requested an environment variable that they could set once and -make all their searches case insensitive in that terminal session. +We'll improve our tool with one extra feature: an option for case insensitive +searching. We could make this a command line option and require they enter it +each time they want it to apply, but instead we're going to use an environment +variable. This allows our users to set the environment variable once and have +all their searches case insensitive in that terminal session. ### Implement and Test a Case-Insensitive `grep` Function First, let's add a new function that we will call when the environment variable -is on. Let's start by adding a new test and re-naming our existing one: +is on. + + + +Let's start by adding a new test and re-naming our existing one: + + ```rust,ignore #[cfg(test)] @@ -1273,9 +1417,11 @@ Trust me."; -We're going to define a new function named `grep_case_insensitive`. Its -implementation will be almost the same as the `grep` function, but with some -minor changes: +We're going to define a new function named `grep_case_insensitive`, shown in +Listing 12-X. It will be almost the same as the `grep` function, but with some +minor changes. We'll lowercase the `search` function and `line` so that, +whatever the case of the input arguments, they will be the same case when +matched. Filename: src/lib.rs @@ -1294,17 +1440,23 @@ fn grep_case_insensitive<'a>(search: &str, contents: &'a str) -> Vec<&'a str> { } ``` +Listing 12-X: The grep_case_insensitive_ function + + First, we lowercase the `search` string, and store it in a shadowed variable with the same name. Note that `search` is now a `String` rather than a string -slice, so we need to add an ampersand when we pass `search` to `contains` since -`contains` takes a string slice. +slice which means that, because it contains takes a string slice, we need to +reference `search` by adding an ampersand when we pass `search` to `contains`. Second, we add a call to `to_lowercase` each `line` before we check if it -contains `search`. Since we've converted both `line` and `search` into all -lowercase, we'll find matches no matter what case they used in the file and the -command line arguments, respectively. Let's see if this passes the tests: +contains `search`. Now we've converted both `line` and `search` to all +lowercase, so we'll find matches no matter what case the user input in the file +and the command line arguments, respectively. + +Let's see if this passes the tests: ```text Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs @@ -1329,8 +1481,8 @@ running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured ``` -Great! Now, we have to actually use the new `grep_case_insensitive` function. -First, let's add a configuration option for it to the `Config` struct: +Great! Now, let's actually use the new `grep_case_insensitive` function. First, +we need to add a configuration option for it to the `Config` struct: Filename: src/lib.rs @@ -1344,8 +1496,9 @@ pub struct Config { -And then check for that option inside of the `run` function, and decide which -function to call based on the value of the `case_sensitive` function: +We add the case_sensitive function that takes a bool. Then we need our `run` +function to be able to check for the case_sensitive function, and for it decide +which function to call based on the value of the `case_sensitive` function: Filename: src/lib.rs @@ -1373,8 +1526,8 @@ pub fn run(config: Config) -> Result<(), Box>{ Finally, we need to actually check the environment for the variable. To bring -the `env` module from the standard library into our project, we add a `use` line -at the top of *src/lib.rs*: +the `env` module from the standard library into our project, we add a `use` +line at the top of *src/lib.rs*: Filename: src/lib.rs @@ -1415,17 +1568,23 @@ impl Config { -Here, we call `env::vars`, which works in a similar way as `env::args`. The -difference is `env::vars` returns an iterator of environment variables rather -than command line arguments. Instead of using `collect` to create a vector of -all of the environment variables, we're using a `for` loop. `env::vars` returns -tuples: the name of the environment variable and its value. We never care about -the values, only if the variable is set at all, so we use the `_` placeholder -instead of a name to let Rust know that it shouldn't warn us about an unused -variable. Finally, we have a `case_sensitive` variable, which is set to true by -default. If we ever find a `CASE_INSENSITIVE` environment variable, we set the -`case_sensitive` variable to false instead. Then we return the value as part of -the `Config`. +Here, we call `env::vars`, which returns an iterator of environment variables +in the same way `env::args` returns an iterator of command line arguments. +Instead of using `collect` to create a vector of all of the environment +variables, we're using a `for` loop. `env::vars` returns two tuples: the name +of the environment variable and its value. + + + +In this case we don't need to know the value of the XXX, only whether the +variable is set, so we use the `_` placeholder instead of a name to let Rust +know that it shouldn't warn us about an unused variable. + +Finally, we have a `case_sensitive` variable, which is set to true by default. +If a `CASE_INSENSITIVE` environment variable is found, the `case_sensitive` +variable is set to false instead. Then we return the value as part of the +`Config`. Let's give it a try! @@ -1447,49 +1606,62 @@ To tell your name the livelong day To an admiring bog! ``` -Excellent! Our `greprs` 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! +Excellent! Our `greprs` 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 both arguments _and_ environment variables for the same -configuration. In those cases, the programs decide that one or the other of -arguments or environment variables take precedence. For another exercise on -your own, try controlling case insensitivity through a command line argument as -well, and decide which should take precedence if you run the program with -contradictory values. +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 a command line argument as well as through the +environement variabble, and decide which should take precedence the program is +run with contradictory values. The `std::env` module contains many more useful features for dealing with environment variables; check out its documentation to see what's available. + + ## Write to `stderr` Instead of `stdout` Right now, we're writing all of our output to the terminal with `println!`. -This works, but most terminals provide two kinds of output: "standard out" is -used for most information, but "standard error" is used for error messages. This -makes it easier to do things like "Print error messages to my terminal, but -write other output to a file." +Most terminals provide two kinds of output: "standard out" for general +information, and "standard error" for error messages. This distinction makes it +easier to direct text; for example we could print error messages to the +terminal, but write other output to a file. + + -We can see that our program is only capable of printing to `stdout` by -redirecting it to a file using `>` on the command line, and running our program -without any arguments, which causes an error: +Let's send some output to a file rather than to the terminal by redirecting the +output using `>` and specifying the file. We run this on the command line with +our program, without any arguments, and if it causes an error: ```text $ cargo run > output.txt ``` + + The `>` syntax tells the shell to write the contents of standard out to -*output.txt* instead of the screen. However, if we open *output.txt* after -running we'll see our error message: +*output.txt* instead of the screen. If we open *output.txt* after running we'll +see our error message: ```text Application error: No search string or filename found ``` -We'd like this to be printed to the screen instead, and only have the output -from a successful run end up in the file if we run our program this way. Let's -change how error messages are printed as shown in Listing 12-15: + + +It's much more useful for error messaages like this to be printed to the screen +instead, and only have the output from a successful run end up in the file. +Let's change how error messages are printed as shown in Listing 12-15: -
Filename: src/main.rs ```rust,ignore @@ -1522,23 +1694,18 @@ fn main() { } ``` -
- Listing 12-15: Writing error messages to `stderr` instead of `stdout` -
-
- Rust does not have a convenient function like `println!` for writing to -standard error. Instead, we use the `writeln!` macro, which is sort of like -`println!`, but it takes an extra argument. The first thing we pass to it is -what to write to. We can acquire a handle to standard error through the -`std::io::stderr` function. We give a mutable reference to `stderr` to -`writeln!`; we need it to be mutable so we can write to it! The second and -third arguments to `writeln!` are like the first and second arguments to -`println!`: a format string and any variables we're interpolating. +standard error. Instead, we use the `writeln!` macro, which is like `println!` +but takes an extra argument. The first thing we pass to it is what to write to. +We can acquire a handle to standard error through the `std::io::stderr` +function. We give a mutable reference to `stderr` to `writeln!`; we need it to +be mutable so we can write to it! The second and third arguments to `writeln!` +are like the first and second arguments to `println!`: a format string and any +variables we're interpolating. Let's try running the program again in the same way, without any arguments and redirecting `stdout` with `>`: @@ -1555,8 +1722,7 @@ try it again with arguments that work: $ cargo run to poem.txt > output.txt ``` -We'll see no output to our terminal, but `output.txt` will contain -our results: +We'll see no output to our terminal, but `output.txt` will contain our results: Filename: output.txt @@ -1567,14 +1733,13 @@ How dreary to be somebody! ## Summary -In this chapter, we've covered how to do common I/O operations in a Rust -context. By using command line arguments, files, environment variables, and the -ability to write to `stderr`, you're now prepared to write command line -applications. By using the concepts from previous chapters, your code will be -well-organized, be able to store data effectively in the appropriate data -structures, handle errors nicely, and be well tested. We also saw a real-world -scenario where lifetime annotations are needed to ensure references are -always valid. - -Next, let's explore how to make use of some features of Rust that were -influenced by functional languages: closures and iterators. +In this chapter, we've recapped on some of the major concepts so far and +covered how to do common I/O operations in a Rust context. By using command +line arguments, files, environment variables, and the `stderr` tool, you're now +prepared to write command line applications. By using the concepts from +previous chapters, your code will be well-organized, be able to store data +effectively in the appropriate data structures, handle errors nicely, and be +well tested. + +Next, let's explore some functional-language influenced Rust features: closures +and iterators.