18 Rust on your computer

This chapter covers

We have seen that it is possible to learn almost anything in Rust just using the Playground. But if you have read this far in the book, you probably already have Rust installed on your computer. And there are always things that you can’t do with the Playground, such as working with files or writing code in more than just one file. Some other things that are best done on your computer for are user input and command-line arguments. But most important is that with Rust on your computer, you can use external crates. We have already learned a few crates, but the Playground only has access to the most popular ones. With Rust on your computer, you can use any external crate at all. The tool that binds this all together is called Cargo, Rust’s package manager.

18.1 Cargo

One of the largest selling points of Rust is that pretty much everyone uses Cargo to build and manage their projects. Using Cargo gives Rust projects a common structure that makes it easy to work with external code written by multiple people at the same time. To understand why Cargo is found almost everywhere in Rust code, let’s first see what writing Rust is like without it.

18.1.1 Why everyone uses Cargo

The Rust compiler is called rustc and is what does the actual compiling. A Rust file ends with an .rs. Technically, you can compile programs on your own with commands like rustc main.rs, but it quickly gets annoying.

But let’s give it a try. Make a new directory and create a new file called test.rs. Then put something simple in like this:

fn main() {
    println!("Does this work?");
}

After that, type rustc test.rs. You should see a file called test.exe. That’s your program! Now just type test, and you should see something like this:

c:\nothing>test
Does this work?
 
c:\nothing>

Not bad! But how do you handle bringing in external code? If we want a random number, we will probably use the rand crate:

use rand::{thread_rng, Rng};
 
fn main() {
    let mut rng = thread_rng();
    println!("Today's lucky number: {}", rng.gen::<u8>());
}

But no luck. A lonely compiler doesn’t know what to do with this sudden rand keyword:

error[E0432]: unresolved import `rand`
 --> test.rs:1:5
  |
1 | use rand::{thread_rng, Rng};
  |     ^^^^ maybe a missing crate `rand`?
  |
  = help: consider adding `extern crate rand` to use the `rand` crate

It is just as confused even if we add extern crate rand as it suggests:

error[E0463]: can't find crate for `rand`
 --> test.rs:1:1
  |
1 | extern crate rand;
  | ^^^^^^^^^^^^^^^^^^ can't find crate

Technically, you can type rustc –help and start looking around for the right way to link external code. But nobody does this when building programs with Rust because there is a package manager and build tool called Cargo that takes care of all of this. Cargo uses rustc to compile, too; it automates the process to make it a nearly painless experience.

One note about the name: it’s called cargo because when you put crates together, you get cargo. A crate is a wooden box that you see on ships or trucks (figure 18.1), but you remember that every Rust project is also called a crate. When you put them together you get the whole cargo. So cargo comes from the idea of putting all the crates together to make a full project.

Figure 18.1 You can think of Cargo as this ship holding all of the external crates together in the same place.

You can see this when you use Cargo to run a project. To start a project in Cargo, type cargo new and its name. For example, you could type cargo new my_project. A directory will be created with the same name, inside of which is Cargo.toml and a directory called /src for the code. Inside this directory is main.rs, which is where you start writing your code. If you want to write a library (i.e., code that is meant for others to use), add --lib to the end of the command. Then Rust will create a lib.rs instead of main.rs in the /src directory.

With a new project started, let’s add rand = "0.8.5" to Cargo.toml, as we learned previously, and write some code to randomly choose between eight letters:

use rand::seq::SliceRandom;     
 
fn main() {
    let my_letters = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
    let mut rng = rand::thread_rng();
    for _ in 0..6 {
        print!("{} ", my_letters.choose(&mut rng).unwrap());
    }
}

This is a blanket trait that lets us use a method called .choose() for slices, so we need to bring it into scope to use it.

This will print something like b c g h e a. But let’s first see what Cargo does before the program starts running. To use Cargo to both build and run a program, type cargo run. But there is quite a bit of output during compiling, too. It will look something like this:

   Compiling rand_core v0.6.4
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling random_test v0.1.0 (C:\rust\random_test)
    Finished dev [unoptimized + debuginfo] target(s) in 2.61s
     Running `target\debug\random_test.exe`

It looks like Cargo didn’t just bring in a single crate called rand, but some others, too. That’s because we need rand for our crate, but rand also has code that needs other crates, too. Cargo will find all the crates we need and put them together. In our case, we only had a few, but on other projects, you may have 200, 600, or sometimes even more crates to bring in. In this case, the program took 2.61 seconds to compile, but this time will, of course, vary.

18.1.2 Using Cargo and what Rust does while it compiles

Compiling time is where you can see the tradeoff for Rust: compiling ahead of time is one reason why Rust is so fast, but you have to wait while Rust compiles your code. However, Rust does use incremental compilation. Incremental compilation means that when you make a change to your code, Rust will only recompile the changes, not the whole program. In our case, imagine that we add the letter i to my_letters and type cargo run again:

let my_letters = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'];

In this case, the crates such as rand_core are already brought in so they don’t get recompiled, and the whole process is a lot quicker:

   Compiling random_test v0.1.0 (C:\rust\random_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target\debug\random_test.exe`
f h i d e d 

This time, it only took 0.55 seconds. So, to speed up your development time, try typing cargo build every time you add an external crate as you work on your code. Rust will compile your code in the background as you work, and every cargo build or cargo run thereafter will be a faster incremental compilation.

Rust optimizes (speeds up) its code in a number of ways, such as by turning generic functions and types into concrete ones. For example, here is some simple generic code that you might write:

use std::fmt::Display;
 
fn print_and_return<T: Display>(input: T) -> T {
    println!("You gave me {input} and now I will give it back.");
    input
}
 
fn main() {
    let my_name = print_and_return("Windy");
    let small_number = print_and_return(9.0);
}

This function can take anything with Display, so we gave it a &str and next gave it an f64, both of which implement Display. However, (unseen to us) the compiler changes generic functions to concrete ones for each type that it will use, which allows the program to be faster at run time.

So when it looks at the first part with "Windy" (a &str), it doesn’t just produce a fn print_and_return<T: Display>(input: T) -> T to use at run time. Instead, it turns it into something like fn print_and_return_str(input: &str) -> &str. It does the same on the next line with the input 9.0, turning the function into something like fn print_and_return_f64(input: f64) -> f64. All this is done during compile time. This is why generic functions take (slightly) longer to compile because the compiler generates a concrete function for each different type to be used at run time.

You’ll sometimes see this called a specialized definition or monomorphization. That is, when the compiler turns the generic function above into something like fn print_ and_return_f64(input: f64) -> f64, it has turned the generic function into a function that is specialized to the f64 type. And monomorphism, which is a Greek term for “single form-ism,” means that the function is now concrete and only has a single form. When we write a generic function, it is polymorphic (“multiple form”) because it can take on a lot of different forms in practice. The compiler then takes these generic functions, specializing them to the input type, and turns them into monomorphic (“single form”) functions.

Thankfully, you don’t have to think about any of this to write your code—the compiler does it all without showing you. But it’s nice to know some of the reasons why Rust takes a while to compile but runs really fast once the compiling is done.

One more thing: the makers of Rust work hard on lowering compile time because compile time is one of Rust’s largest pain points. However, almost every version of Rust compiles a bit faster than the previous version, and Rust today compiles much faster than it did a few years ago. If you are curious about some of these details, check out this blog by a Rust developer who writes about recent compiler improvements in a pretty readable way: https://nnethercote.github.io/.

Here are the most basic commands about Cargo to know:

The best way to see the difference between debug and release mode is to look at a small function like this that uses a loop that runs 1,000 times:

pub fn add() -> i32 {
    let mut sum = 0;
    for _ in 0..1000 {
        sum += 1
    }
    sum
}

You’ll notice that the code is asking the computer to do a lot of work (looping 1,000 times), but the code is pretty simple. Even humans can look at this and know what the final output will be. Rust can do this sometimes, too.

In Debug mode, the compiler will quickly put the code together to run this loop at run time, as well as add some debug info. The focus in debug mode is on compiling quickly and helping the developer. You can see this if you paste this function into the website Godbolt (https://rust.godbolt.org/), which shows the assembly code generated. For this function, you’ll see 100+ lines of code generated. Even if you don’t know any assembly, you’ll notice that the compiler is generating the code needed to run the loop. Figure 18.2 shows there are a lot of terms like Iterator, Range, into_iter, PartialOrd, and so on, so quite a bit of code relating to iterators and comparing numbers.

Figure 18.2 Compiling in debug mode takes less time but the code itself ends up doing more work.

You can see the debug info at the end of the file, too, such as an error message in case the number overflows and the program needs to panic (figure 18.3).

Figure 18.3 And some parts of the compiled code in debug mode are easily readable.

Now, here comes the fun part: release mode. Click on the triangle on the top right next to Compiler Options, select -C opt-level=val, and change val to 3 (3 is the optimization level for release builds). The compiler will then try to optimize as much as possible (figure 18.4)—and now the assembly is only three lines long!

Figure 18.4 Release mode takes a lot longer, but the compiled code is most efficient.

The compiler has spent some extra time in release mode to analyze the loop and sees that it will always return 1,000. So why bother adding any extra code at run time? That is essentially how optimization at compile time works. If you choose to spend the extra time to compile in release mode, the compiler will have the extra time to analyze the code and shorten it as much as possible.

Here are some more Cargo commands:

By the way, the --release part of the command is called a “flag.” That means extra information in a command.

Let’s finish up this section with a few more useful Cargo commands:

18.2 Working with user input

Now that we have Rust installed, we can work with user input. Generally, there are two ways to do this: while the program is running through stdin (that is, through the user’s keyboard) and before the program runs through command line arguments.

18.2.1 User input through stdin

The easiest way to take input from the user is with std::io::stdin. This is pronounced “standard in,” which in this case is the input from the keyboard. With the stdin() function, you can get a Stdin struct, which is a handle to this input and has a method called .read_line() that lets you read the input to a &mut String. Here is a simple example of that, which is a loop that continues forever until the user presses the x key. It sort of works, but not quite in the right way. If you are feeling adventurous, try running the code yourself on your computer and think about why it doesn’t quite work as expected:

use std::io;
 
fn main() {
    println!("Please type something, or x to escape:");
    let mut input_string = String::new();
 
    while input_string != "x" {
        input_string.clear();                               
        io::stdin().read_line(&mut input_string).unwrap();  
        println!("You wrote {input_string}");
    }
    println!("See you later!");
}

First, clear the String during every loop. Otherwise, it will just get longer and longer.

Then use read_line to read the input from the user into read_string.

Here is some possible output:

Please type something, or x to escape:
something
You wrote something
 
Something else
You wrote Something else
 
x
You wrote x
 
x
You wrote x

It takes our input and gives it back, and it even knows that we typed x. But it doesn’t exit the program. The only way to get out is by closing the window or by typing Ctrl + C to shut the program down. Did you notice the space after the output that says, “You wrote x”? That’s a hint. Let’s change the {} to {:?} in println!() to see whether there is any more more information. Doing this shows us what is going on:

Please type something, or x to escape:
something
You wrote "something\r\n"
Something else
You wrote "Something else\r\n"
x
You wrote "x\r\n"
x
You wrote "x\r\n"

Ah ha! This is because the keyboard input is actually not just something; it is something and the Enter key. When pressing Enter, Windows will add a \r\n (a carriage return and a new line), while other operating systems will add a \n (new line). In either case, we aren’t getting a simple x output when we press x to exit the program.

There is an easy method to fix this called .trim(), which removes all the whitespace. Whitespace, by the way, is defined as any of these characters (https://doc.rust-lang.org/reference/whitespace.html):

U+0009 (horizontal tab, '\t')
U+000A (line feed, '\n')
U+000B (vertical tab)
U+000C (form feed)
U+000D (carriage return, '\r')
U+0020 (space, ' ')
U+0085 (next line)
U+200E (left-to-right mark)
U+200F (right-to-left mark)
U+2028 (line separator)
U+2029 (paragraph separator)

Using .trim() will turn x\r\n (or x\n) into just x. Now it works:

use std::io;
 
fn main() {
    println!("Please type something, or x to escape:");
    let mut input_string = String::new();
 
    while input_string.trim() != "x" {
        input_string.clear();
        io::stdin().read_line(&mut input_string).unwrap();
        println!("You wrote {input_string}");
    }
    println!("See you later!");
}

Now it will print

Please type something, or x to escape:
something
You wrote something
 
Something
You wrote Something
 
x
You wrote x
 
See you later!

The std::io module has a lot of other structs (https://doc.rust-lang.org/std/io/index.html#structs) and methods if you need finer control over user input and program output.

With that quick introduction to user input done, let’s take a look at another type of user input: input that happens before the program even starts.

18.2.2 Accessing command-line arguments

Rust has another kind of user input called std::env::Args. This Args struct holds what the user types when starting the program, known as command-line arguments. There is actually always at least one Arg in a program, no matter what the user types. Let’s write a program that only prints them using std::env::args() to see what it is:

fn main() {
    println!("{:?}", std::env::args());
}

If we type cargo run, it prints something like this:

Args { inner: ["target\\debug\\rust_book.exe"] }

You can see that Args will always give you the name of the program, no matter what.

Let’s give it more input and see what it does. Try typing cargo run but with some extra words. It gives us

Args { inner: ["target\\debug\\rust_book.exe", "but", "with", "some",
"extra", "words"] }

So it looks like every word after cargo run is recognized and can be accessed via this args() method. When we look at the documentation for Args (https://doc.rust-lang.org/std/env/struct.Args.html), we see that it implements IntoIterator, which is quite convenient. So we can just put it in a for loop:

use std::env::args;
 
fn main() {
    let input = args();
    for entry in input {
        println!("You entered: {}", entry);
    }
}

Now it says

You entered: target\debug\rust_book.exe
You entered: but
You entered: with
You entered: some
You entered: extra
You entered: words

Since the first argument is always the program name, you will often want to skip it. We can do that with the .skip() method that all iterators have:

use std::env::args;
 
fn main() {
    let input = args();
    input.skip(1).for_each(|item| {
        println!(
            "You wrote {item}, which in capital letters is {}",
            item.to_uppercase()
        );
    })
} 

The code will print

You wrote but, which in capital letters is BUT
You wrote with, which in capital letters is WITH
You wrote some, which in capital letters is SOME
You wrote extra, which in capital letters is EXTRA
You wrote words, which in capital letters is WORDS

We can do more with these command line arguments inside our program besides print them. They are just strings, so it is easy enough to check to see if any arguments have been entered, and match on them if an argument is found. Here’s a small example that either makes letters big (capital) or small (lowercase):

use std::env::args;
 
enum Letters {
    Capitalize,
    Lowercase,
    Nothing,
}
 
fn main() {
    let mut changes = Letters::Nothing;
    let input = args().collect::<Vec<_>>();
 
    if let Some(arg) = input.get(1) {
        match arg.as_str() {
            "capital" => changes = Letters::Capitalize,
            "lowercase" => changes = Letters::Lowercase,
            _ => {}
        }
    }
 
    for word in input.iter().skip(2) {
      match changes {
        Letters::Capitalize => println!("{}", word.to_uppercase()),
        Letters::Lowercase => println!("{}", word.to_lowercase()),
        _ => println!("{}", word)
      }
    }
}

Let’s look at some examples of input. Try to imagine what will be printed out.

Input: cargo run please make capitals:

In this case, it will look at index 1, which is please. This input please doesn’t match capital or lowercase, so it will print out the remaining words without any change:

make capitals

Input: cargo run capital

In this case, it will match as before, but there is nothing after index 1 to print out, so there is no output.

Now, let’s look at some arguments from a user who is starting to figure out how the program works:

Input: cargo run capital I think I understand now

I
THINK
I
UNDERSTAND
NOW

Input: cargo run lowercase Does this work too?

does
this
work
too?

In practice, command-line arguments are used in a pretty similar way for most command-line interfaces (CLIs). An example of this is cargo run --help, which Cargo recognizes as a request to print out a menu to help the user know which commands are available. The main crate used by Rust users to work with command-line arguments is known as clap (CLAP = Command Line Argument Parser; https://docs.rs/clap/latest/clap/), which is highly recommended if you are putting together a CLI that needs to take in a lot of different types of arguments and flags.

18.2.3 Accessing environment variables

Besides Args, there are also Vars, which are environment variables. Those can be seen when using std::env::args() and are the basic settings for the operating system and program that the user didn’t type in. These variables will include information like URLs.

Even the simplest program will have a lot of environment variables that vary by computer. Using std::env::vars() allows you to see them all as a (String, String) (a key and a value). Let’s take a look at what the Vars on the Rust Playground look like:

fn main() {
    for (key, value) in std::env::vars() {
        println!("{key}: {value}");
    }
}

There’s quite a bit!

CARGO: /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo
CARGO_HOME: /playground/.cargo
CARGO_MANIFEST_DIR: /playground
CARGO_PKG_AUTHORS: The Rust Playground
CARGO_PKG_DESCRIPTION: 
CARGO_PKG_HOMEPAGE: 
CARGO_PKG_LICENSE: 
CARGO_PKG_LICENSE_FILE: 
CARGO_PKG_NAME: playground
CARGO_PKG_REPOSITORY: 
CARGO_PKG_VERSION: 0.0.1
CARGO_PKG_VERSION_MAJOR: 0
CARGO_PKG_VERSION_MINOR: 0
CARGO_PKG_VERSION_PATCH: 1
CARGO_PKG_VERSION_PRE: 
DEBIAN_FRONTEND: noninteractive
HOME: /playground
HOSTNAME: 637927f45315
LD_LIBRARY_PATH: /playground/target/debug/build/libsqlite3-sys-
7c00a5831fa0c673/out:/playground/target/debug/build/ring-
c92344ea3efaac76/out:/playground/target/debug/deps:/playground/target
/debug:/playground/.rustup/toolchains/stable-x86_64-unknown-linux-
gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib:/playground
/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib
PATH: /playground/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr
/sbin:/usr/bin:/sbin:/bin
PLAYGROUND_EDITION: 2021
PLAYGROUND_TIMEOUT: 10
PWD: /playground
RUSTUP_HOME: /playground/.rustup
RUSTUP_TOOLCHAIN: stable-x86_64-unknown-linux-gnu
RUST_RECURSION_COUNT: 1
SHLVL: 1
SSL_CERT_DIR: /usr/lib/ssl/certs
SSL_CERT_FILE: /usr/lib/ssl/certs/ca-certificates.crt
USER: playground
_: /usr/bin/timeout

Environment variables can also be set while a program is running using std::env::set_var(). The following code will add an extra key and value for each existing key and value, except with an exclamation mark at the end:

fn main() {
    for (mut key, mut value) in std::env::vars() {
        key.push('!');
        value.push('!');
        std::env::set_var(key, value);
    }
    for (key, value) in std::env::vars() {
        println!("{key}: {value}");
    }
}

The output will show that there are now twice as many environment variables, half of which have exclamation marks everywhere. Here is part of the output:

CARGO!: /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo!
CARGO_HOME!: /playground/.cargo!
CARGO_MANIFEST_DIR!: /playground!
CARGO_PKG_AUTHORS!: The Rust Playground!

Now, let’s take a look at a more real example of set_var().

Programs often send logging information to an external service that displays the data in a nice format that makes it easy to understand. Most logging in Rust uses the RUST_LOG environment variable to keep track of how detailed logs should be. The five main logging levels are

You can see these logging levels in crates like env_logger (https://docs.rs/env_logger/latest/env_logger/).

Services are generally first deployed to a Dev environment for developers to work on and test, in which case RUST_LOG will be set to DEBUG, or maybe even TRACE. Then, when the developers are more confident in the service, they will move it to Prod (production), which will probably use a quieter logging level like INFO—but not always.

The following code represents a case where an app starting up will first check for RUST_LOG, and if nothing is set, will check to see if some other environment variable (we’ll call it "LOGGER_URL") shows a url to send logging information to. If the LOGGER_URL matches the dev url, it will assume that the logging level is DEBUG, and if LOGGER_URL matches the prod url, it will assume that the logging level is INFO. And if neither of these can be found, then RUST_LOG will be set to INFO:

use std::env;
const DEV_URL: &str = "www.somedevurl.com";
const PROD_URL: &str = "www.someprodurl.com";
 
fn main() {
    match std::env::var("RUST_LOG") {
        Ok(log) => println!("Logging at {log} level"),
        Err(_) => match std::env::var("LOGGER_URL") {
            Ok(url) if url == DEV_URL => {
                println!("Dev url indicated, defaulting to debug");
                env::set_var("RUST_LOG", "DEBUG");
            }
            Ok(url) if url == PROD_URL => {
                println!("Prod url indicated, defaulting to info");
                env::set_var("RUST_LOG", "INFO");
            }
            _ => {
                println!("No valid url indicated, defaulting to info");
                env::set_var("RUST_LOG", "INFO");
            }
        },
    }
}

If run on the Playground, you will see this output, showing that the environment variable hasn’t been set:

No valid url indicated, defaulting to info

You probably didn’t find this section particularly difficult. Working with user input usually involves more thinking about how your software works than writing code that you need to work hard at to compile. There are, of course, many other forms of user input. The last two chapters of the book include working with instantaneous user input (e.g., keyboard presses), so feel free to take a look at those chapters if you are curious and want to know now.

18.3 Using files

With Rust installed on the computer, we can now start working with files. You will notice that a lot of this code involves working with Results. This makes sense, as many things can go wrong when it comes to working with files. A file might not even exist, maybe the computer can’t read it, or you might not have permission to access it. All of these possible things that can go wrong make the ? operator really handy when working with files.

18.3.1 Creating files

Let’s try working with files for the first time. The std::fs module contains methods for working with files, and with the std::io::Write trait in scope, you can write to them. With that, we can use .write_all() to write into the file. Here is a simple example that creates a file and writes some data to it:

use std::fs;
use std::io::Write;
 
fn main() -> std::io::Result<()> {
    let mut file = fs::File::create("myfilename.txt")?;    
    file.write_all(b"Let's put this in the file")?;        
    Ok(())
}

Creates a file with this name. Be careful! If you have a file with this name already, it will be deleted.

Files take bytes, so don’t forget the b in front.

Then, if you click on the new file myfilename.txt, you can see the Let's put this in the file text inside.

We don’t even need to use two lines to do this, though, thanks to the question mark operator. It will pass on the result we want if it works, kind of like when you chain methods on an iterator:

use std::fs;
use std::io::Write;
 
fn main() -> std::io::Result<()> {
    fs::File::create("myfilename.txt")?
        .write_all(b"Let's put this in the file")?;
    Ok(())
}

In fact, there is also a function that does both of these things together. It’s called std::fs::write(). Inside it, you give it the file name you want and the content you want to put inside. Again, careful! It will delete everything in that file if it already exists. It even lets you write a &str without b in front because write() takes anything that implements AsRef<[u8]> and str implements AsRef<[u8]>:

pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()>

This makes it very simple:

use std::fs;
 
fn main() -> std::io::Result<()> {
    fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't they have color film back then?
Dad: Sure they did. In fact, those photographs are in color. It's just the world was black and white then.")?;
    Ok(())
}

18.3.2 Opening existing files

Opening a file is just as easy as creating one. You just use open() instead of create(). After that (if the program finds your file), you can use methods like read_to_ string(), which lets you read the contents of a file into a String. It looks like this:

use std::fs;
use std::fs::File;
use std::io::Read;                                                    
 
fn main() -> std::io::Result<()> {
     fs::write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't
they have color film back then?
Dad: Sure they did. In fact, those photographs are in color. It's just the
world was black and white then.")?;
 
    let mut calvin_file = File::open("calvin_with_dad.txt")?;         
    let mut calvin_string = String::new();                            
    calvin_file.read_to_string(&mut calvin_string)?;                  
    calvin_string.split_whitespace().for_each(|word| print!("{} ",
           word.to_uppercase()));                                     
    Ok(())
}

This is to use the function .read_to_string().

Opens the file we just made

This String will hold the contents of the file.

Reads the file into the String using the read_to_string method

Now that we have it as a String, we’ll capitalize the whole thing just for fun.

That will print

CALVIN: DAD, HOW COME OLD PHOTOGRAPHS ARE ALWAYS BLACK AND WHITE? DIDN'T
THEY HAVE COLOR FILM BACK THEN? DAD: SURE THEY DID. IN FACT, THOSE
PHOTOGRAPHS ARE IN COLOR. IT'S JUST THE WORLD WAS BLACK AND WHITE
THEN.

18.3.3 Using OpenOptions to work with files

What if we only want to create a file if there is no other file with the same name? This would let us avoid deleting any existing files when trying to make a new one. The std::fs module has a struct called OpenOptions that lets us do this, along with other custom behavior.

Interestingly, we’ve been using OpenOptions all this time and didn’t even know it. The source code for File::open() shows us the OpenOptions struct being used to open a file:

pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
        OpenOptions::new().read(true).open(path.as_ref())
    }

That looks familiar! It’s the builder pattern that we learned in chapter 15. The same pattern shows up inside the File::create() method:

pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
    OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref
    ())
    }

So, it looks like OpenOptions has a lot of methods to set whether to carry out certain actions when working with files. If you go to the documentation page for OpenOptions (https://doc.rust-lang.org/std/fs/struct.OpenOptions.html), you can see all the methods that you can choose from. Most take a bool:

Then, at the end, you use .open() with the filename, and that will give you a Result.

Since Rust 1.58, you can access this OpenOptions struct directly from File through a method called options(). In the next example, we will make an OpenOptions with File::options(). Then we will give it the ability to write. After that, we’ll set .create_ new() to true and try to open the file we made. It won’t work, which is what we want:

use std::fs::{write, File};
 
fn main() -> std::io::Result<()> {
    write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't
they have color film back then?
Dad: Sure they did. In fact, those photographs are in color. It's just the
world was black and white then.")?;
 
    let calvin_file = File::options()
        .write(true)
        .create_new(true)
        .open("calvin_with_dad.txt")?;
    Ok(())
}

The error shows us that the file already exists, so the program exits with an error:

Error: Os { code: 80, kind: AlreadyExists, message: "The file exists." }

Next, let’s try using .append() so we can write to an existing file. We’ll also use the write! macro this time, which is yet another option available to us. We saw this macro before when implementing Display for our structs:

use std::fs::{read_to_string, write, File};
use std::io::Write;
 
fn main() -> std::io::Result<()> {
    write("calvin_with_dad.txt", 
"Calvin: Dad, how come old photographs are always black and white? Didn't
they have color film back then?
Dad: Sure they did. In fact, those photographs are in color. It's just the
world was black and white then.")?;
 
    let mut calvin_file = File::options()
        .append(true)
        .read(true)
        .open("calvin_with_dad.txt")?;
    calvin_file.write_all(b"Calvin: Really?\n")?;
    write!(&mut calvin_file, "Dad: Yep. The world didn't turn color until
    sometime in the 1930s...\n")?;
    println!("{}", read_to_string("calvin_with_dad.txt")?);
    Ok(())
}

Thanks to the ability to append, the file now holds a bit more of the conversation between Calvin and his dad:

Calvin: Dad, how come old photographs are always black and white? Didn't
they have color film back then?
Dad: Sure they did. In fact, those photographs are in color. It's just the
world was black and white then.
Calvin: Really?
Dad: Yep. The world didn't turn color until sometimes in the 1930s... 

Finally, Rust has a convenient macro called include_str! that simply pulls the contents of a file into a &'static str at compile time—right into the binary. If the file can’t be found, the program won’t compile. This next sample will simply take the contents of main.rs and print it out:

fn main() {
    // Text, text, text
    let main = include_str!("main.rs");
    println!("Here's what main.rs looks like:\n\n{main}");
}

So the include_str! macro not only gives compile-time checking and a file conveniently located in memory but also increases the size of the binary. For example, try copying the contents of Bram Stoker’s Dracula (https://www.gutenberg.org/files/345/345-h/345-h.htm) into a file, use include_str!(), and then type cargo build. The file size inside the target/debug directory should be about 999 KB. But if you use std::fs::read_to_string() instead, you will have to access the file and handle the error (or unwrap) at run time, but the file size should be a much smaller 166 KB. In other words, the include part of the name of the macro refers to including the content inside the binary:

fn main() {
    let content = include_str!("dracula.txt");                          
    // let content = std::fs::read_to_string("dracula.txt").unwrap();   
}

999 KB

166 KB

As you can see, opening and writing to files isn’t particularly difficult. Just be careful that you don’t end up deleting existing files when creating a new one. Starting with File::options() is good default behavior to make sure that you are reviewing how you want your program to react when it comes across files with the same name.

18.4 cargo doc

You might have noticed that Rust documentation looks almost the same whether the code is from the standard library or someone else’s external crate. The left side of the documentation shows structs and traits, code examples are on the right, and so on in pretty much every crate you can find. This is because you can automatically make documentation just by typing cargo doc, and this convenience leads to almost everyone using it.

Even making a project with just a simple struct or two can help you learn about traits in Rust. For example, here are two structs that do almost nothing and nothing else:

pub struct DoesNothing {}
pub struct PrintThing {}
 
impl PrintThing {
    pub fn prints_something() {
        println!("I am printing something");
    }
}

With just two empty structs and one method, you would think that cargo doc would generate just the struct names and one method. But if you type cargo doc --open (--open will open up the documentation in your browser once it is done), you can see a lot more information than you expected. The front page looks like figure 18.5, which does look fairly empty.

Figure 18.5 Cargo doc makes your documentation look professional even if all you did was make two empty structs.

But if you click on one of the structs, it will show you a lot of traits that you didn’t think were there. If you click on the DoesNothing struct, it will show us quite a few traits even though we didn’t type a single word of code to implement them. First, we see a number of traits that are automatically implemented:

Auto Trait Implementations
impl RefUnwindSafe for DoesNothing
impl Send for DoesNothing
impl Sync for DoesNothing
impl Unpin for DoesNothing
impl UnwindSafe for DoesNothing

And after that come some blanket implementations, which we learned about in the last chapter:

Blanket Implementations
impl<T> Any for T where T: 'static + ?Sized,
impl<T> Borrow<T> for T where T: ?Sized,
impl<T> BorrowMut<T> for T where T: ?Sized,
impl<T> From<T> for T
impl<T, U> Into<U> for T where U: From<T>,
impl<T, U> TryFrom<U> for T where U: Into<T>,
impl<T, U> TryInto<U> for T where U: TryFrom<T>

Then, if we add some documentation comments with /// you can see them when you type cargo doc. Here is the same code with a few comments above each struct and method:

/// This is a struct that does nothing
pub struct DoesNothing {}
 
/// This struct only has one method.
pub struct PrintThing {}
 
impl PrintThing {
    /// This function just prints a message.
    pub fn prints_something() {
        println!("I am printing something");
    }
}

These comments will now show up in the documentation (figure 18.6).

Figure 18.6 Structs with comments added

When you click on PrintThing, it will show this struct’s methods as well (figure 18.7).

Figure 18.7 Showing methods via PrintThing

cargo doc is particularly nice when using a lot of external code. Because these crates are all on different websites, it can take some time to search them all. But if you use cargo doc, you will have them all in the same place on your hard drive. If you don’t want to document all the external code, you can pass in a --no-deps (no dependencies) flag, which will only compile your code.

Cargo is one of the main reasons for Rust’s popularity as a language, and after this chapter, you can probably see why. It allows you to start a project, build your code, document it, check it, add external crates, and much more. With Rust and Cargo installed by now, we were also able to take in user input and command-line arguments for the first time and work with files.

With Rust installed, we can also start to do HTTP requests, and in the next chapter, we will do just that with the reqwest crate that we first saw in the last chapter. Not only will we learn how the crate works, but it will also give us our first introduction to async Rust: Rust code that doesn’t block its thread while doing some work. We’ll also learn how to use feature flags to only take in part of an external crate, allowing us to shorten compile time a little.

Summary