Variables, Types and Functions · about 40 min

Reading Input: stdin and Command-Line Arguments

Why this matters

A program that always prints the same thing is not very useful. Real programs take input: text typed or piped in, file names and options on the command line. From here on most exercises give your program input, so this lesson teaches the handful of input recipes you will use constantly. Some pieces (String, &mut, Vec, unwrap) are explained fully in later modules; here you get an accurate short version of each, enough to use them with confidence.

Standard input

When a program starts, standard input (stdin) is connected to the keyboard, or to a file or another program if the shell redirected it:

$ ./prog              # stdin is the terminal: type, then press Ctrl-D to end input
$ ./prog < data.txt   # stdin is the file
$ echo 42 | ./prog    # stdin is the output of echo

The exercise checker feeds each test case's input to your program this way.

Reading one line

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    println!("{:?}", line);
    let name = line.trim();
    println!("Hello, {name}!");
}
"Ada\n"
Hello, Ada!

Piece by piece:

read_line returns Ok(0) and leaves the string empty at end of input; that is how you detect that there is nothing more to read.

Turning text into numbers

Input is always text. To get a number, parse it:

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: i64 = line.trim().parse().unwrap();
    println!("{n} squared is {}", n * n);
}
12 squared is 144

parse can produce many types (integers of every size, floats, bool, char...), so it needs to know which one you want. Here it learns from the annotation let n: i64. Without an annotation, you write the type on the call with the "turbofish" syntax: line.trim().parse::<i64>().

parse returns a Result too, because the text might not be a number. And note the order: trim() first, since "12\n" with its newline is not a valid number.

fn main() {
    let ok: Result<i32, _> = "42".parse::<i32>();
    let bad = "4 2".parse::<i32>();
    let newline = "42\n".parse::<i32>();
    println!("{:?}", ok);
    println!("{:?}", bad);
    println!("{:?}", newline);
}
Ok(42)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })

Ok(42) is a success holding 42; Err(...) is a failure holding a description of the problem. If you unwrap() an Err, the program stops:

fn main() {
    let n: i32 = "12a".trim().parse().unwrap();
    println!("{n}");
}
thread 'main' panicked at main.rs:2:39:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

called Result::unwrap() on an Err value is the panic message you will see whenever input is not what your code assumed. Handling it gracefully is module 8.

Reading many lines

To process input line by line until it ends, loop over lines():

use std::io;

fn main() {
    let mut total = 0;
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let n: i32 = line.trim().parse().unwrap();
        total += n;
        println!("read {n}, total so far {total}");
    }
    println!("total: {total}");
}
read 5, total so far 5
read 10, total so far 15
read -2, total so far 13
total: 13

io::stdin().lines() produces the lines one at a time, without their newline characters, and the for loop runs its body once for each. Each item is a Result (reading might fail), hence line.unwrap(). The loop ends when input ends. The next module covers for properly; this pattern is worth memorizing as is.

Alternatively, read everything at once into one String and work with it:

use std::io::{self, Read};

fn main() {
    let mut input = String::new();
    io::stdin().read_to_string(&mut input).unwrap();
    let words = input.split_whitespace().count();
    println!("{} bytes, {} words", input.len(), words);
}
26 bytes, 5 words

use std::io::{self, Read}; imports both io itself and the Read trait, which provides read_to_string (traits are module 10; for now, "this method needs that use"). If you forget it, the compiler tells you which use to add.

Command-line arguments

Arguments come from the command that started the program: ./prog input.txt 3. Collect them into a list:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{} arguments: {:?}", args.len(), args);
    let first = &args[1];
    let times: u32 = args[2].parse().unwrap();
    println!("{first} x {times}");
}
3 arguments: ["./prog", "hello", "3"]
hello x 3

Common mistakes

Forgetting trim:

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: i32 = line.parse().unwrap();
    println!("{n}");
}
thread 'main' panicked at main.rs:6:31:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

InvalidDigit for input that looks perfectly numeric: the invisible \n is the invalid digit.

Reusing one String for several read_line calls. read_line appends, so the second number ends up after the first in the same String ("3\n4\n"), and parsing fails. Create a new String per line, or call line.clear() before reading again.

Forgetting mut on the String: read_line must modify it, and the compiler reports E0596, "cannot borrow line as mutable, as it is not declared as mutable".

Leaving out the type for parse:

fn main() {
    let n = "5".parse().unwrap();
    println!("{n}");
}
error[E0284]: type annotations needed
 --> main.rs:2:9
  |
2 |     let n = "5".parse().unwrap();
  |         ^       ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `n` an explicit type
  |
2 |     let n: /* Type */ = "5".parse().unwrap();
  |          ++++++++++++

E0284 "type annotations needed": tell it, with let n: i32 or parse::<i32>().

Off-by-one on arguments: args[0] is the program name, not your first argument.

Recap

Going further: in the sandbox shell, compile a program with rustc and try ./prog < /etc/hostname, printf '1\n2\n' | ./prog, and running it with no arguments to see each failure mode. The std::io module documentation lists BufRead, which you will use for efficient line reading later.

Exercises

Exercise 1 · write code

Echo a name

Read one line from standard input (a name) and print a greeting. For input Ada the output is:

Hello, Ada!

Make sure the newline at the end of the input does not end up inside your output.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut name = String::new();
    io::stdin().read_line(&mut name).unwrap();
    println!("Hello, {name}!");
}

Example

input

Ada

expected output

Hello, Ada!

Example

input

  Grace Hopper

expected output

Hello, Grace Hopper!
Hint 1

Print the input with {:?} to see what is in it.

Hint 2

let name = name.trim(); removes the trailing newline (and surrounding spaces).

Reference solution
use std::io;

fn main() {
    let mut name = String::new();
    io::stdin().read_line(&mut name).unwrap();
    let name = name.trim();
    println!("Hello, {name}!");
}

Exercise 2 · write code

Add two numbers from input

The input has two lines, each holding one integer (possibly negative, possibly with spaces around it). Print their sum and their product on two lines:

sum: 7
product: 12

for input 3 and 4. Use i64.

File: main.rs

Starting point

use std::io;

fn main() {
}

Example

input

3
4

expected output

sum: 7
product: 12

Example

input

 -5 
  20

expected output

sum: 15
product: -100
Hint 1

Each read_line call reads one more line into the String you give it (appending!). Use a fresh String for each line.

Hint 2

line.trim().parse::<i64>().unwrap() turns the text into a number.

Hint 3

A helper fn read_number() -> i64 keeps main short.

Reference solution
use std::io;

fn read_number() -> i64 {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    line.trim().parse().unwrap()
}

fn main() {
    let a = read_number();
    let b = read_number();
    println!("sum: {}", a + b);
    println!("product: {}", a * b);
}

Exercise 3 · write code

Command-line arguments

The program is run with exactly two arguments: a word and a count, for example ./prog ab 3. Print the word repeated count times on one line, then the total length:

ababab
length: 6

args[1] is the word and args[2] the count. "ab".repeat(3) builds "ababab", and .len() gives a string's length in bytes (all inputs here are plain ASCII).

File: main.rs

Starting point

use std::env;

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

Example with arguments ab 3

expected output

ababab
length: 6

Example with arguments xyz 1

expected output

xyz
length: 3
Hint 1

args[0] is the program's own name; your arguments start at index 1.

Hint 2

repeat wants a usize: let count: usize = args[2].parse().unwrap();

Reference solution
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let word = &args[1];
    let count: usize = args[2].parse().unwrap();
    let line = word.repeat(count);
    println!("{line}");
    println!("length: {}", line.len());
}

Exercise 4 · write code

Sum every line

Read standard input until it ends. Every line holds one integer. Print how many numbers there were and their total:

count: 4
total: 100

For empty input, print count: 0 and total: 0. Use for line in io::stdin().lines() as shown in the lesson (the for loop is covered properly in the next module).

File: main.rs

Starting point

use std::io;

fn main() {
    let mut count = 0;
    let mut total: i64 = 0;
    // loop over the lines here
    println!("count: {count}");
    println!("total: {total}");
}

Example

input

10
20
30
40

expected output

count: 4
total: 100

Example

expected output

count: 0
total: 0
Hint 1

Each item from lines() must be unwrapped: let line = line.unwrap();

Hint 2

Inside the loop, parse the line and add it to total, and add 1 to count.

Reference solution
use std::io;

fn main() {
    let mut count = 0;
    let mut total: i64 = 0;
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let n: i64 = line.trim().parse().unwrap();
        count += 1;
        total += n;
    }
    println!("count: {count}");
    println!("total: {total}");
}

Exercise 5 · quiz

What does unwrap do?

let n: i32 = text.trim().parse().unwrap(); runs with text equal to "12a". What happens?

Answer
  • The program panics with a message containing ParseIntError { kind: InvalidDigit }

parse returns a Result: either the number or an error describing why parsing failed. It does not guess. unwrap() says "give me the number, and if it is an error, panic". That is fine for learning and for quick tools, and wrong for programs facing real users. Module 8 shows how to handle the error instead, for example by printing a friendly message and exiting with status 1.