In this lesson we’re going to learn how to write data to a file. We’ll also briefly touch on the traits that enable writing.
let mut file = File::create(filename).unwrap();
file.write_all(new_file_contents.as_bytes()).unwrap();
The Code
We’re going to end up with this code.
use std::{env, fs::File, io::Write};
fn main() {
let args: Vec<String> = env::args().collect();
let layout = &args[1];
let tags = &args[2];
let heading = &args[3];
let filename = &args[4];
let new_file_contents = format!(
"---
layout: {layout}
tags: {tags}
status: draft
---
# {heading}
"
);
let mut file = File::create(filename).unwrap();
file.write_all(new_file_contents.as_bytes()).unwrap();
}
Writing to a File
We now have access to an instance of the File struct in the file variable we created in the last lesson.
Looking at the docs for the File struct we can see a number of functions that we can call in the sidebar, including metadata for getting file metadata.
There is no function for writing to a file though.
This is because the function for writing lives in the Write trait.
Traits are a way of defining functionality that can be shared by any number of types. This is similar to interfaces in other languages.
The Write trait, for example, defines function signatures for a set of functions related to writing. We can then implement the Write trait for File, which will allow us to call write on a File.
Write exists as a trait because a File isn’t the only struct we can write to, we can also write to Stdout, for example.
Luckily for us, we don’t have to do anything but bring the trait into scope to use functions from the Write trait on File because the implementations are already written.
write_all takes an exclusive reference to self, which is the File, and a slice of u8. which is commonly called a “byte slice” because a u8 is one byte. This function comes from the Write trait and will attempt to write the entire byte slice that we give it to the file.
It returns a Result which is similar to what we saw with the create function, but the Ok value is going to be (), pronounced “unit”, which is a value that we can’t use for anything useful. The returned Result only exists to tell us if the write succeeded or not.
We’ll walk through the potential errors one by one. Start with this code, which calls write_all on the file, and tries to pass in the new_file_contents as an argument. We’ll .unwrap the Result so that out program will crash if we failed to write to the file.
file.write_all(new_file_contents).unwrap();
If we run our binary
cargo run post rust,bevy "Doing shader stuff in Bevy" doing-shader-stuff-in-bevy.md
then we see a compile error that tells us it can’t find the method write_all for File.
If we read further, it suggests bringing std::io::Write into scope, which is the trait that contains the write_all function.
❯ cargo run post rust,bevy "Doing shader stuff in Bevy" doing-shader-stuff-in-bevy.md
Compiling first v0.1.0 (/rust-adventure/first-cli)
error[E0599]: no method named `write_all` found for struct `File` in the current scope
--> src/main.rs:24:10
|
24 | file.write_all(new_file_contents).unwrap();
| ^^^^^^^^^ method not found in `File`
|
::: /Users/chris/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/io/mod.rs:1540:8
|
1540 | fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
| --------- the method is available for `File` here
|
= help: items from traits can only be used if the trait is in scope
help: the following trait is implemented but not in scope; perhaps add a `use` for it:
|
1 | use std::io::Write;
|
At the top of the file, we can bring the Write trait into scope alongside the other items.
use std::{env, fs::File, io::Write};
If we try again, we see a type mismatch. write_all expects the byte slice (&[u8]) as an argument, but we gave it new_file_contents, which is a String.
❯ cargo run post rust,bevy "Doing shader stuff in Bevy" doing-shader-stuff-in-bevy.md
Compiling first v0.1.0 (/rust-adventure/first-cli)
error[E0308]: mismatched types
--> src/main.rs:24:20
|
24 | file.write_all(new_file_contents).unwrap();
| --------- ^^^^^^^^^^^^^^^^^ expected `&[u8]`, found struct `String`
| |
| arguments to this function are incorrect
|
note: associated function defined here
--> /Users/chris/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/io/mod.rs:1540:8
|
1540 | fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
| ^^^^^^^^^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `first` due to previous error
String has a function called as_bytes which gives us the byte slice for the String.
So we update our code
file.write_all(new_file_contents.as_bytes()).unwrap();
and try again. This time we’re told we can’t borrow the file variable as mutable, because it isn’t declared as mutable.
The Rust compiler also gives us a hint: “consider changing this to be mutable”.
❯ cargo run post rust,bevy "Doing shader stuff in Bevy" doing-shader-stuff-in-bevy.md
Compiling first v0.1.0 (/rust-adventure/first-cli)
error[E0596]: cannot borrow `file` as mutable, as it is not declared as mutable
--> src/main.rs:24:5
|
23 | let file = File::create(filename).unwrap();
| ---- help: consider changing this to be mutable: `mut file`
24 | file.write_all(new_file_contents.as_bytes()).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `first` due to previous error
In this case, that is the right fix. If we’re going to mutate the data owned by a variable, which write_all tells us it does, then we need to declare that variable as mut.
let mut file = File::create(filename).unwrap();
file.write_all(new_file_contents.as_bytes()).unwrap();
Now if we run our program, we see a file created with the appropriate frontmatter and heading at the location we gave.
---
layout: post
tags: rust,bevy
status: draft
---
# Doing shader stuff in Bevy