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