We have a title for the post and a directory to put the new file in so let's create a new file.
Let's start by concatenating the output directory with the title of the post, followed by .md
.
Concatenating strings in Rust
Concatenating strings together in Rust is idiomatically done by using the format!
macro.
fn main() {
let args = Args::parse();
dbg!(args);
let filename =
format!("{}/{}.md", args.output_dir, args.title);
}
This macro allows us to use a formatting string as the first argument, and values to place into the formatting string as the next arguments.
In this case we're replacing {}
, the Display
formatter, with the output directory and the title of the blog post.
If we run the code above, we immediately notice a compilation error.
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
error[E0382]: borrow of moved value: `args`
--> src/main.rs:35:43
|
32 | let args = Args::parse();
| ---- move occurs because `args` has type `Args`, which does not implement the `Copy` trait
33 | dbg!(args);
| ---------- value moved here
34 | let filename =
35 | format!("{}/{}", args.output_dir, args.title);
|
We're using args in dbg!
already!
value borrowed here after move
Because we're using the args variable in dbg!
, we've essentially given the value to that function call. This is often phrased as having “moved” the value in the variable into the function.
This happens because Rust has the requirement that any value can only have one owner. If the function owns the value, then the variable can't also own the value at the same time, and we certainly can't use it after we've given it away to the function.
The solution here is straightforward: We share a reference to the value instead of passing the whole value in to dbg!
.
Using the &
means using a shared reference. We're basically saying “here, dbg!
, take a look at this value and let me know when you're done looking at it”.
This means that the args
variable retains ownership of the value, and after dbg!
prints the value out, we still have access to it in args
.
fn main() {
let args = Args::parse();
dbg!(&args);
let filename =
format!("{}/{}.md", args.output_dir, args.title);
}
Creating a File
Rust has an fs
module in the standard library. Inside of this module is the write
function.
At the top of the file, bring the fs
module into scope using use
.
use std::fs;
use clap::Parser;
and use the filename as the file and the title of the post as the file content in our main function.
fn main() {
let args = Args::parse();
dbg!(&args);
let filename =
format!("{}/{}.md", args.output_dir, args.title);
fs::write(filename, args.title);
}
Running the program with cargo run
compiles and runs the program with a few warnings.
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
warning: unused `Result` that must be used
--> src/main.rs:37:5
|
37 | fs::write(filename, args.title);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
37 | let _ = fs::write(filename, args.title);
| +++++++
warning: `scaffold` (bin "scaffold") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/scaffold`
[src/main.rs:33] &args = Args {
layout: "post",
tags: [],
title: "A Post",
status: "draft",
output_dir: "content",
}
unused Result that must be used
exists as a warning to prevent exactly what just happened to us.
Our file creation isn't actually working.
Result and io::Result
fs::write
returns a Result<()>
. The Result type in Rust is often used to communicate errors back to the user of the function.
In our case, the warning is telling us that we didn't check to see if the Result succeeded or failed, so we wouldn't know either way, and in this case our file has actually failed to write to disk!
The std::result::Result
type is an enum made up of two variantsL Ok
and Err
.
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok
usually wraps a successful value, while Err
wraps some additional information if an error occurred.
This type is used all over the place in Rust code, so the T
and E
identifiers are used to hold the place of “whatever the user's success and error values are”.
Rust's standard library uses the std::io::Error
type as the error type for all the errors in the io
module.
Because this error type is used in so many places, Rust has a type alias named the same thing at std::io::Result
.
So the Result<()>
type from fs::write
is actually a std::result::Result<(), std::io::Error>
.
That is, ()
is the success type and std::io::Error
is the error type.
()
is a non-value. It's pronounced “unit” and there's nothing we can do with it, so it only exists to make it ok to return an Ok
.
Using a Result
We have a number of different ways to handle the result and make the error go away.
The easiest by far is to use .unwrap()
or .expect()
. They both do the same thing:
- If the Result value is an
Err
variant, then panic the program. - If it is an
Ok
variant, then unwrap the Result and return the underlying success value.
unwrap()
is fine to use when you're just learning. As you advance in your Rust usage, you'll want to use functions that panic
less and less.
Use .unwrap()
for now, and we'll come back to handle it again later.
fs::write(filename, args.title).unwrap();
run the program to see the error
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
Finished dev [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/scaffold`
[src/main.rs:32] &args = Args {
layout: "post",
tags: [],
title: "A Post",
status: "draft",
output_dir: "content",
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:36:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
On line 36, the error message says "No such file or directory"
.
We used a default value of content
as our output_dir
, so this one is easy for us to diagnose.
If we create the content
directory ourselves, then our CLI will succeed. I'll create the directory on the command line using mkdir
.
❯ mkdir content
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/scaffold`
[src/main.rs:32] &args = Args {
layout: "post",
tags: [],
title: "A Post",
status: "draft",
output_dir: "content",
}
and now the file content/A Post.md
exists with the content A Post
.