How to Parse Command Output in Rust

Cli
Convert string input to numbers in Rust with .parse(), the Result type, and a match expression — the basics every learner trips over and how to handle parse failures cleanly.

You're writing your first guessing game in Rust. The user types a number, your program reads it from the terminal, and now you want to compare it to the secret. There's just one problem: what came in is text. The string "42" is not the integer 42, and Rust will absolutely refuse to let you pretend otherwise. So how do you cross that bridge?

This is one of the first places a new Rust programmer meets the language's stricter side. Python's happy to let int(input()) either work or blow up at runtime. Rust says: nope, you're going to acknowledge, in code, that parsing might fail. The good news is the tool for it is small and easy once you've seen it twice.

The core idea: parse returns a Result

Every string in Rust has a method called .parse(). You ask it to turn the string into some other type (a number, an IP address, a UUID, anything that knows how to read itself from text), and it gives back a Result. A Result is Rust's way of saying "this might have worked, or it might have failed, and you have to deal with both cases before you get the value out."

Think of it like a sealed envelope. Inside is either the parsed number you wanted, or a little note explaining why it couldn't be parsed. You can't peek through the envelope. You have to open it, and the act of opening it forces you to decide what to do if the note is bad news.

// Read a line from stdin first
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();

// .trim() strips the trailing newline that read_line includes
// .parse() returns Result<u32, ParseIntError>
let guess: u32 = match input.trim().parse() {
    Ok(num) => num,        // success: bind num and use it as the value
    Err(_) => {
        println!("That wasn't a number.");
        return;            // bail out cleanly on bad input
    }
};

println!("You guessed: {}", guess);

A few things are doing real work here. The type annotation let guess: u32 is what tells .parse() which kind of number to produce; without it, the compiler can't decide whether you meant a u8, an i32, an f64, or any other numeric type. The match is the canonical way to take a Result apart. Ok(num) is the "happy path" arm: when parsing succeeded, the parsed number is named num and the whole match expression evaluates to it. Err(_) is the "something went wrong" arm. The underscore says "I don't care about the specific error, I just need to handle the failure."

Why .trim() matters

When you read a line from stdin, the newline character at the end comes along for the ride. So if the user types 42 and presses Enter, you actually have the string "42\n" sitting in your buffer. .parse::<u32>() on "42\n" fails, because \n is not a digit. .trim() returns a string slice with whitespace stripped from both ends, so you hand "42" to .parse() and it works.

This trips people up constantly. The error message is something like invalid digit found in string, which sounds mysterious until you remember the invisible newline.

Inside a loop: the continue pattern

In the classic Rust Book guessing game, the parse lives inside a loop, and bad input shouldn't crash the program. It should just ask again. That's where you see this idiom:

loop {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();

    // If parse fails, skip this iteration and prompt again
    let guess: u32 = match input.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,    // throw out garbage, go back to the top
    };

    // Now we have a real u32; compare it
    if guess == 42 {
        println!("You got it!");
        break;
    } else {
        println!("Try again.");
    }
}

Read the match as: "give me the number if parsing worked; otherwise jump straight back to the top of the loop." continue is an expression in Rust, and it has the type "never returns," which means the compiler is happy to let it stand in for a u32 value in the Err arm. Sounds wild, but it falls out of the type system naturally: if a branch never produces a value, it can't violate any expectation about what that value should be.

Don't reach for unwrap or expect in real code

You'll see two shortcuts in tutorials:

// Both of these crash the program if parsing fails.
let a: u32 = "42".parse().unwrap();
let b: u32 = "42".parse().expect("expected a number");

unwrap() says "give me the inner value, and if it's an error, panic and tear down the program." expect("...") is the same thing with a custom panic message. They're fine for throwaway scripts, prototypes, and examples. They're not fine when a user types something weird and you'd rather print a polite message than crash.

A quick hierarchy:

  • Throwaway script that you alone will run: unwrap() is fine.
  • Code that takes user input or reads files: handle the Err properly.
  • Library code that other people import: never panic on bad input; return a Result from your own function and let the caller decide.

A more realistic example

Say you're writing a tiny tool that reads a list of port numbers from a config line like "80,443,8080,8443" and wants to validate each one. Here's how a thoughtful version handles parse errors without crashing.

// Returns Ok with the parsed ports, or Err describing the first bad token.
fn parse_ports(input: &str) -> Result<Vec<u16>, String> {
    let mut ports = Vec::new();

    // .split(',') yields each comma-separated chunk
    for token in input.split(',') {
        // Trim each token in case the user wrote "80, 443, 8080"
        let trimmed = token.trim();

        // .parse::<u16>() returns Result<u16, ParseIntError>
        // map_err converts the integer-parse error into our own String error
        let port: u16 = trimmed
            .parse()
            .map_err(|e| format!("bad port {:?}: {}", trimmed, e))?;

        ports.push(port);
    }

    Ok(ports)
}

fn main() {
    match parse_ports("80, 443, 8080, hello") {
        Ok(p)  => println!("got ports: {:?}", p),
        Err(e) => eprintln!("error: {}", e),
    }
}

The interesting move here is the ? operator at the end of the parse line. ? is sugar for "if this is Ok, give me the value; if it's Err, return that error from the enclosing function right now." It only works inside functions that return a Result, which is why parse_ports returns one. This pattern, parse-and-bubble-up, is how serious Rust code propagates failures cleanly without tower-of-match-statements.

The map_err call upgrades the boring ParseIntError into a friendlier message that names the offending token. ParseIntError is informative on its own, but when you're parsing a list, you almost always want to mention which item went wrong.

Common pitfalls

Forgetting the type annotation. .parse() doesn't know what to parse to until you tell it. If you write let guess = input.trim().parse();, the compiler will refuse:

error[E0282]: type annotations needed
  --> src/main.rs:5:9
   |
5  |     let guess = input.trim().parse();
   |         ^^^^^
   |
help: consider giving `guess` an explicit type
   |
5  |     let guess: /* Type */ = input.trim().parse();

Two fixes: name the type on the left (let guess: u32 = ...) or use the turbofish on .parse (input.trim().parse::<u32>()). Both work; pick whichever reads more clearly in context.

Forgetting to trim. As mentioned. The newline from read_line will silently kill your parse. If you see invalid digit found in string and your input "looks fine," 90% of the time the answer is .trim().

Parsing into too small a type. "300".parse::<u8>() returns an Err with the message number too large to fit in target type. A u8 only holds 0-255. Pick a wider integer or validate range separately.

Negative numbers into unsigned types. "-5".parse::<u32>() is an error: invalid digit found in string, because the minus sign isn't a digit and u32 is unsigned. If your input might be negative, parse to a signed type like i32 first.

Beyond numbers

.parse() works for anything that implements the FromStr trait. That includes bool, char, IP addresses (std::net::IpAddr), SocketAddr, and many third-party types like uuid::Uuid or chrono::NaiveDate. The pattern is identical: call .parse(), give a target type, handle the Result. Once you've internalized the muscle memory, you reach for it without thinking.

use std::net::IpAddr;

let ip: IpAddr = "192.168.1.1".parse().expect("not a valid IP");
let flag: bool = "true".parse().unwrap_or(false);

Where to go next