---
title: "20.1 Command-line arguments"
description: "Reading the arguments a user passes to your program with std::env::args, collecting them into a Vec, and the convention of argument zero."
url: "https://learnrust.net/chapter-20/command-line-arguments/"
last_updated: "2026-06-13"
---

# 20.1 Command-line arguments

Every program you've written so far has been a closed box: it runs, maybe asks the user a question with `read_line`, and prints. Real command-line tools are configured from the *outside*, by the words you type after the program's name. `grep needle file.txt`, `cargo build --release`, `ls -la`: the `needle`, the `file.txt`, the `--release`, the `-la` are **command-line arguments**, and a program reads them to know what to do. This chapter builds a real tool, and the first thing any tool needs is its arguments.

## Getting the arguments

The arguments live in the standard library's `env` module, behind the function `std::env::args`. It returns an iterator (chapter 19, now immediately useful) over the arguments as `String`s. The usual move is to `collect` them into a `Vec`:

```rust
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{args:?}");
}
```

Build it and run it with some arguments. If the compiled program is called `args_demo`:

```bash
$ cargo run -- apple banana cherry
```

```
["target/debug/args_demo", "apple", "banana", "cherry"]
```

Two things to notice. First, `collect` needs the `Vec<String>` annotation, the exact missing-type situation from lesson [19.5](https://learnrust.net/chapter-19/consuming-iterators/). Second, and this surprises everyone the first time: the *first* element isn't `apple`. It's the path to the program itself. Argument zero is, by long-standing convention across operating systems, the name the program was invoked as. The arguments *you* care about start at index 1.

> **The -- in cargo run**
>
> `cargo run -- apple banana` has a `--` in it. That separator tells `cargo` "everything after this is for *my program*, not for you." Without it, `cargo` would try to interpret `apple` as one of its own options. When you run the compiled binary directly (`./target/debug/args_demo apple banana`), there's no `cargo` in the way and no `--` needed. The `--` is a cargo-ism, not part of your program's argument handling.

## Skipping the program name

Because argument zero is the program name, real code usually skips it. Since `args()` is an iterator, the cleanest way is the `skip` adapter from chapter 19, or to pull the first item off and ignore it. Most often you just index past it:

```rust
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        println!("usage: greet <name>");
        return;
    }

    let name = &args[1];
    println!("Hello, {name}!");
}
```

```bash
$ cargo run -- Ada
```

```
Hello, Ada!
```

The `args.len() < 2` check matters. If the user runs the program with no arguments, `args` holds only the program name (length 1), and reaching for `args[1]` would panic with an out-of-bounds index, the chapter-18 lesson that `[]` panics, here triggered by user behavior rather than a bug. Checking the length first and printing a usage message is the polite, non-panicking response, and it's what every well-behaved tool does. (Chapter 12's `get` returning `Option` is the other way to handle a possibly-missing index; for arguments, a length check reads clearly.)

## Arguments are always text

One more thing to internalize early, because it shapes the whole chapter. Every argument arrives as a `String`, always, even when it looks like a number. If your tool takes a count or a port number, you get the *text* `"8080"`, and turning it into a number is your job, with `parse` (lesson [5.6](https://learnrust.net/chapter-5/parsing-strings-into-numbers/)):

```rust
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        println!("usage: double <number>");
        return;
    }

    match args[1].parse::<i64>() {
        Ok(n) => println!("{}", n * 2),
        Err(_) => println!("'{}' is not a number", args[1]),
    }
}
```

```bash
$ cargo run -- 21
42
$ cargo run -- banana
'banana' is not a number
```

This is the same text-to-number border you crossed back in chapter 5, now arriving from the command line instead of from `read_line`. The user can type anything, so `parse` returns a `Result`, and a real tool reports the bad input instead of crashing. The whole pattern of chapter 12, handle the error path, applies directly to argument parsing, and the project at the end of this chapter leans on it.

> **Non-UTF-8 arguments**
>
> `env::args()` panics if an argument isn't valid UTF-8 (rare, but possible with odd filenames on some systems). For the everyday tools in this chapter that's fine and `args()` is the right choice. If you ever need to accept *any* bytes a shell can pass, there's a sibling `env::args_os()` that yields `OsString` values instead, sidestepping the UTF-8 requirement. You'll almost never need it; reach for it only if a real filename ever makes `args()` panic.

## Quiz time

**Question #1**

What does the first element of `env::args().collect::<Vec<String>>()` contain, and where do the user's arguments begin?

<details class="solution">
<summary>Show solution</summary>

The first element (index 0) is the path/name the program was invoked as, by operating-system convention, not the first real argument. The arguments the user actually passed begin at index 1. That's why argument-reading code typically skips index 0 (with `.skip(1)` or by indexing from 1).

</details>

**Question #2**

Why is it important to check `args.len()` before indexing `args[1]`?

<details class="solution">
<summary>Show solution</summary>

If the user runs the program with no arguments, `args` contains only the program name (length 1), so `args[1]` is out of bounds and `[]` indexing *panics* (chapter 18). Checking the length first lets you print a usage message and exit cleanly instead of crashing. It's the difference between a bug and a handled "you forgot an argument" case.

</details>

**Question #3**

A tool takes a port number as its first argument. The user runs it with `8080`. What type is `args[1]`, and what must you do to use it as a number?

<details class="solution">
<summary>Show solution</summary>

`args[1]` is a `String` containing the text `"8080"`, command-line arguments always arrive as text, never as numbers. To use it numerically you must `parse` it (e.g. `args[1].parse::<u16>()`), which returns a `Result` because the user might have typed something that isn't a number. Handle the `Err` case (print a message) rather than `unwrap`ing, since the input is user-controlled.

</details>

Arguments tell a tool *what* to work on, and most often what they name is a file. The next lesson ([20.2](https://learnrust.net/chapter-20/reading-and-writing-files/)) covers reading and writing files: the one-line conveniences for small jobs and the buffered approach for big ones.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
