Accept Command-Line Arguments

RustBeginner
Practice Now

Introduction

A command-line program receives values after its executable name, writes normal results to standard output, and reports failures through standard error and a nonzero exit status. Those process boundaries let people and scripts tell success from failure.

You will complete a small text-search command. Its search function is already prepared in the package's library source, so each step can focus on argument parsing, file loading, no-match behavior, and final process communication.

Parse the Command-Line Arguments

In this step, you will validate and extract a search query and file path from the process arguments.

The project is /home/labex/project/mini-search. src/main.rs is the command-line binary, while src/lib.rs contains prepared reusable search logic for a later step. Enter the project and open the binary source:

cd /home/labex/project/mini-search
nano src/main.rs

env::args() produces every argument as a String, including the executable path at index zero. The prepared main collects them into a vector. It then passes &args to the parameter args: &[String].

The type &[String] is a slice of String values: a read-only borrowed window over the vector's elements. It follows the same borrowing idea as &str, but it views a sequence of String elements rather than bytes of text. The vector remains owned by main while run reads its elements.

The data flow is:

shell words → env::args() → Vec<String> in main → borrowed &[String] in run

Replace the placeholder Err(...) inside run with:

    if args.len() != 3 {
        return Err(String::from("usage: mini-search <query> <file>"));
    }
    let query = args[1].clone();
    let path = args[2].clone();
    Ok(vec![format!("Query: {query}"), format!("File: {path}")])

Exactly three entries means the executable name plus two user arguments. The explicit return Err(...) stops run immediately when that shape is wrong. This is an early return, unlike the final-expression returns you used in the functions Lab: code below it runs only when the argument count is valid.

Indexing the borrowed slice yields borrowed strings. The two clone calls intentionally create owned copies of only the query and path so the rest of run can manage them as local owned values. This is a deliberate ownership choice, not a general fix for move errors. The vec![first, second] macro then creates a two-element vector, and Ok temporarily returns those two inspection lines.

Save with Ctrl+O, press Enter, and exit with Ctrl+X. Run the command with two arguments:

cargo run --quiet -- rust data/notes.txt

The first --quiet reduces Cargo's own messages. The separate -- tells Cargo to stop interpreting options; everything after it is passed to your program.

Query: rust
File: data/notes.txt

This proves the arguments arrived in the expected positions.

Read the File and Call Library Logic

In this step, you will replace the temporary inspection result with real file loading and search results.

Open the binary source:

nano src/main.rs

Add these imports below use std::env;:

use std::fs;
use mini_search::find_lines;

The package name mini-search becomes the Rust crate name mini_search. The prepared pub on find_lines makes that function accessible outside the library crate. Importing it lets the binary call the public function from src/lib.rs; keeping search logic there separates reusable data processing from process-specific argument handling. This is a small preview of the library/binary and visibility model taught fully in the later modules Lab.

Replace the temporary Ok(vec![...]) line with:

    let contents = fs::read_to_string(&path)
        .map_err(|error| format!("could not read {path}: {error}"))?;
    Ok(find_lines(&query, &contents))

The read error retains the requested path and is propagated with ?. On success, the binary borrows the query and file contents while the library returns owned matching lines.

Save and exit, then check and run:

cargo check
cargo run --quiet -- rust data/notes.txt
Rust makes ownership explicit.
Cargo builds Rust packages.
Rust tools help beginners.

Normal search results are printed to standard output by the prepared Ok arm in main.

Turn an Empty Search into a Useful Error

In this step, you will distinguish a successful search with results from a valid search that found nothing.

Open the source:

nano src/main.rs

Replace Ok(find_lines(&query, &contents)) with:

    let matches = find_lines(&query, &contents);
    if matches.is_empty() {
        return Err(format!("no lines matched '{query}'"));
    }
    Ok(matches)

An empty vector is observable, but the command is more useful when it explains that outcome. Save and exit, then try a missing term:

cargo run --quiet -- python data/notes.txt
no lines matched 'python'

At this intermediate stage, the prepared Err arm still prints through standard output and exits successfully. The next step will give errors their correct process behavior.

Send Errors to Stderr and Exit Nonzero

In this step, you will complete the command-line boundary by separating normal output from failures.

Standard output, or stdout, carries requested results. Standard error, or stderr, carries diagnostics independently. A zero exit status means success; a nonzero status means failure. Open the binary source:

nano src/main.rs

Add this import below the existing standard-library imports:

use std::process;

Replace the one-line Err(error) arm with:

        Err(error) => {
            eprintln!("{error}");
            process::exit(1);
        }

eprintln! writes a line to stderr. process::exit(1) immediately finishes the process with status one. Save and exit, then build once so you can run the binary directly without Cargo adding its own failure message:

cargo build --quiet
./target/debug/mini-search python data/notes.txt

The diagnostic remains:

no lines matched 'python'

Immediately print the previous command's status:

echo $?

echo prints its arguments, and the shell expands $? to the most recent command's exit status:

1

The same boundary now covers invalid usage and missing files. Running ./target/debug/mini-search reports the usage message; a path such as data/missing.txt reports the read error. Both write only to stderr and exit with status one, while a matching search writes results to stdout and exits zero.

Summary

You collected and validated command-line arguments, used Cargo's -- separator, kept search logic in a prepared library boundary, loaded the requested file, and completed conventional stdout, stderr, and exit-status behavior for success and three failure cases.