In this lesson we’re going to learn how to access the arguments passed to a binary when its executed, and debug the values we get out to the console.
❯ ./target/debug/first one something 42 "with spaces"
[src/main.rs:5] args = [
"./target/debug/first",
"one",
"something",
"42",
"with spaces",
]
The code
We’re going to end up with this code and on the way we’ll work our way through some of the issues you’ll see while developing a Rust application.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
Importing… kinda.
Like many languages, Rust has a standard library of functions and other items that we can use if we want to. To get access to the arguments passed to our binary, we’ll use the std::env::args()
function.
The colons are the import path. As it turns out, the dependencies we use in Rust are already available to our application if we use the right name for them. In this case we’re using the args
function that is located in the env
submodule of the std
library.
We can call the args
function using this long-form name, effectively specifying the full module path to the function.
fn main() {
std::env::args();
}
Common Mistake #1
One really common mistake is not including the semi-colon at the end of our expression. That would look like this.
fn main() {
std::env::args()
}
std::env::args()
returns a value (because we want to retrieve the arguments), so omitting the semi-colon will cause the value returned from our main
function to be the value args()
returns: Args
.
This is because Rust is an expression-based language, and the last expression in the function is the return value of the function.
The error message looks like this. Let’s learn how to read it.
❯ cargo run
Compiling first v0.1.0 (/rust-adventure/first)
error[E0308]: mismatched types
--> src/main.rs:2:5
|
1 | fn main() {
| - expected `()` because of default return type
2 | std::env::args()
| ^^^^^^^^^^^^^^^^- help: consider using a semicolon here: `;`
| |
| expected `()`, found struct `Args`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `first` due to previous error
Firstly there is an error code and an error type: error[E0308]: mismatched types
. The Rust team maintains a page of all of the compiler errors. Error 308 can be found on that site which contains a few examples that could cause it to happen. This information is also available on the command line, as indicated at the bottom of the error message: rustc --explain E0308
.
rustc
is the Rust compiler, which Cargo calls when it wants to compile our program, so it was installed when you installed Rust.
Rust then prints out the section of our code that had the issue. In this case since it’s so small, the output is our entire program.
The error itself is expected () found struct Args
. Args
is the return value of args()
, which we can see on the documentation page.
Which leaves us with figuring out what ()
is. The error message says that Rust expected () because of default return type
. We didn’t specify a return type for our main
function, so Rust made the return type ()
by default. Not writing a return type is just a convenience for us programmers.
The Rust compiler helpfully gives us a potential solution. consider using a semicolon here
Why a semicolon?
In other languages, semi-colons are often seems as window dressing. Something that can be left in or out and not affect the program.
In Rust, semicolons have a purpose. Since Rust is an expression-based language, every expression can return a value. Semicolons are an operator that throws away the return value for us.
So by using a semicolon on the end of std::env::args
, we’re throwing away the Args
value intentionally. Since there’s no value left, the value returned from an expression with a semicolon at the end is ()
.
fn main() {
std::env::args();
}
Common Mistake #2
After including our semicolon, we run immediately into a new warning. Note that this isn’t an error. Our program still compiled and ran just fine.
Rust includes a set of high-signal warnings as part of the compiler. These are almost always an error in our program.
lower-signal or optional warnings and lints go into a tool called Clippy.
One such warning is the must_use
warning. This indicates a value that must be used in the program (yes, Rust can detect whether or not we use a value in our program in many cases). Often this warning is implemented because if the value isn’t used, it doesn’t do anything and we almost always want to do something.
In this case, it’s because std::env::args
returns a type of value that implements the Iterator
trait. Iterators let us iterator over a collection of values, in this case the arguments to our binary.
Iterators are lazy and don’t do anything unless called. So if we don’t iterate over this Iterator, we will never actually fetch any values.
❯ cargo run
Compiling first v0.1.0 (/rust-adventure/first)
warning: unused `Args` that must be used
--> src/main.rs:2:5
|
2 | std::env::args();
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: iterators are lazy and do nothing unless consumed
warning: `first` (bin "first") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/first`
Collect
We have a few different ways to solve this. The one we’re going to use in this lesson is collect
. collect
is a fantastic function that is defined on Iterator
s. It allows us to collect the values the Iterator produces into some container.
collect
can collect into an amazing variety of containers. Because collect
is one of the few Rust functions that is so extremely generic, we have to specify what type to build when we collect our values.
In this case we’re going to use a Vec
. Vec
s are the Rust equivalent of an array in JavaScript. We can push new values onto a Vec
, iterate over the values, etc.
fn main() {
let args: Vec<String> = std::env::args().collect();
}
We’ll define a new variable named args
that has a type of Vec<String>
. Rust can infer the type of this variable, and collect
will know how to handle it.
Debugging with the dbg! macro
If we run the program at this point, Rust tells us that we didn’t use args
. This is a different warning than the one we got before though. The one we got before was pretty critical. This one gives us the opportunity to ignore it by using an _
if we meant to not use it.
❯ cargo run
Compiling first v0.1.0 (/rust-adventure/first)
warning: unused variable: `args`
--> src/main.rs:2:9
|
2 | let args: Vec<String> = std::env::args().collect();
| ^^^^ help: if this is intentional, prefix it with an underscore: `_args`
|
= note: `#[warn(unused_variables)]` on by default
warning: `first` (bin "first") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/first`
However, we’ll use it so that we can see some new output from our CLI.
fn main() {
let args: Vec<String> = std::env::args().collect();
dbg!(args);
}
dbg!
is another macro that we can use to debug our program. It functions much the same as println!
but instead of printing out nice strings, dbg!
will print out the Debug
representations of our values. This includes things that are good for programmers like line numbers, variable names, and more.
In this case we’re debugging out the variable args
on line 3
in src/main.rs
.
❯ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/first`
[src/main.rs:3] args = [
"target/debug/first",
]
The value of args
is "target/debug/first"
, which feels strange since we didn’t pass anything in yet.
The first argument in any CLI is always the name of the binary that is being called. If we change directories into target/debug
and run the first
binary we just built, we’ll see the name change to ./first
.
first
❯ cd target/debug/
first/target/debug
❯ ./first
[src/main.rs:3] args = [
"./first",
]
In most programs this value is just skipped over.
Running with arguments
We can use cargo run
to run our binary and pass some arguments in.
❯ cargo run one something 42 "with spaces"
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/first one something 42 'with spaces'`
[src/main.rs:3] args = [
"target/debug/first",
"one",
"something",
"42",
"with spaces",
]
The arguments are debugged out to the console for us to inspect.
The Module System
Finally before we move on, we can introduce use
.
Maybe typing std::env::args()
feels a bit long to do. We can use use
to shorten the module path. For example, we can use std::env
at the top of our file, which brings the env
submodule into scope, which we can then use as before with the new shorter name: env::args
.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
We haven’t changed anything about our application’s behavior, this is simply a programmer’s convenience when we don’t want to type out longer module paths.