Accepting Command Line Arguments
Let’s create a new project with, as always, cargo new
. We’ll call our project
minigrep
to distinguish it from the grep
tool that you might already have
on your system.
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
The first task is to make minigrep
accept its two command line arguments: the
file path and a string to search for. That is, we want to be able to run our
program with cargo run
, two hyphens to indicate the following arguments are
for our program rather than for cargo
, a string to search for, and a path to
a file to search in, like so:
$ cargo run -- searchstring example-filename.txt
Right now, the program generated by cargo new
cannot process arguments we
give it. Some existing libraries on crates.io can help
with writing a program that accepts command line arguments, but because you’re
just learning this concept, let’s implement this capability ourselves.
Reading the Argument Values
To enable minigrep
to read the values of command line arguments we pass to
it, we’ll need the std::env::args
function provided in Rust’s standard
library. This function returns an iterator of the command line arguments passed
to minigrep
. We’ll cover iterators fully in Chapter 13. For now, you only need to know two details about iterators: iterators
produce a series of values, and we can call the collect
method on an iterator
to turn it into a collection, such as a vector, that contains all the elements
the iterator produces.
The code in Listing 12-1 allows your minigrep
program to read any command
line arguments passed to it and then collect the values into a vector.
Filename: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
First, we bring the std::env
module into scope with a use
statement so we
can use its args
function. Notice that the std::env::args
function is
nested in two levels of modules. As we discussed in Chapter
7, in cases where the desired function is
nested in more than one module, we’ve chosen to bring the parent module into
scope rather than the function. By doing so, we can easily use other functions
from std::env
. It’s also less ambiguous than adding use std::env::args
and
then calling the function with just args
, because args
might easily be
mistaken for a function that’s defined in the current module.
The
args
Function and Invalid UnicodeNote that
std::env::args
will panic if any argument contains invalid Unicode. If your program needs to accept arguments containing invalid Unicode, usestd::env::args_os
instead. That function returns an iterator that producesOsString
values instead ofString
values. We’ve chosen to usestd::env::args
here for simplicity, becauseOsString
values differ per platform and are more complex to work with thanString
values.
On the first line of main
, we call env::args
, and we immediately use
collect
to turn the iterator into a vector containing all the values produced
by the iterator. We can use the collect
function to create many kinds of
collections, so we explicitly annotate the type of args
to specify that we
want a vector of strings. Although we very rarely need to annotate types in
Rust, collect
is one function you do often need to annotate because Rust
isn’t able to infer the kind of collection you want.
Finally, we print the vector using the debug macro. Let’s try running the code first with no arguments and then with two arguments:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
Notice that the first value in the vector is "target/debug/minigrep"
, which
is the name of our binary. This matches the behavior of the arguments list in
C, letting programs use the name by which they were invoked in their execution.
It’s often convenient to have access to the program name in case you want to
print it in messages or change behavior of the program based on what command
line alias was used to invoke the program. But for the purposes of this
chapter, we’ll ignore it and save only the two arguments we need.
Saving the Argument Values in Variables
The program is currently able to access the values specified as command line arguments. Now we need to save the values of the two arguments in variables so we can use the values throughout the rest of the program. We do that in Listing 12-2.
Filename: src/main.rs
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {}", query);
println!("In file {}", file_path);
}
As we saw when we printed the vector, the program’s name takes up the first
value in the vector at args[0]
, so we’re starting arguments at index 1
. The
first argument minigrep
takes is the string we’re searching for, so we put a
reference to the first argument in the variable query
. The second argument
will be the file path, so we put a reference to the second argument in the
variable file_path
.
We temporarily print the values of these variables to prove that the code is
working as we intend. Let’s run this program again with the arguments test
and sample.txt
:
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
Great, the program is working! The values of the arguments we need are being saved into the right variables. Later we’ll add some error handling to deal with certain potential erroneous situations, such as when the user provides no arguments; for now, we’ll ignore that situation and work on adding file-reading capabilities instead.