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:
use std::io;brings the modulestd::io(input/output, from the standard library) into scope, so you can writeio::stdin()instead ofstd::io::stdin().String::new()creates an empty, growable piece of text. Unlike a string literal, aStringcan be changed, so the binding ismut. Its characters live on the heap (module 4).io::stdin()gives a handle to standard input..read_line(&mut line)reads up to and including the next newline and appends it toline.&mut linelends the String toread_linewith permission to modify it; module 5 is all about such borrows..unwrap(): reading can fail (for example if the input is not valid UTF-8 text).read_linereturns aResult, a value that is either a success or an error.unwrap()extracts the success and panics on an error. Treat it for now as "assume this works, crash with a message if not".- The Debug print shows the catch: the line includes its newline
\n. .trim()returns the text without leading and trailing whitespace (spaces, tabs, newlines). Forgettingtrimis the most common input bug.
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
env::args()produces the arguments one by one;.collect()gathers them into aVec<String>, a growable list of Strings (module 9). The annotation tellscollectwhat to build.args[0]is the program's own path, so your arguments start at index 1.- Indexing a
Vecis checked just like an array: running with too few arguments panics with "index out of bounds". &args[1]borrows the argument rather than taking it out of the list (module 5). Arguments are text, so numbers must be parsed.
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
io::stdin().read_line(&mut s)appends one line, newline included, to aString.io::stdin().lines()in aforloop gives each line without its newline;read_to_stringreads everything.- Always
trim()beforeparse(). Say the target type with an annotation orparse::<T>(). parseand reading returnResult;unwrap()takes the value or panics. Proper handling: module 8.env::args().collect::<Vec<String>>()gives the arguments; index 0 is the program itself.
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.