Step 7 of 7
Putting it together: a config reader
Real programs combine everything from this module. Here's a reader for a small config file: blank lines and # comments are skipped, every other line must be key = number. Bad lines are reported with their line number, and the program keeps going.
use std::io::BufRead;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (key, value) = line
.split_once('=')
.ok_or_else(|| String::from("expected key = value"))?;
let key = key.trim();
if key.is_empty() {
return Err(String::from("missing key"));
}
let value = value
.trim()
.parse::<u32>()
.map_err(|e| format!("bad value for {key}: {e}"))?;
Ok((key.to_string(), value))
}
fn main() {
let mut number = 0;
let mut settings = 0;
let mut errors = 0;
for line in std::io::stdin().lock().lines() {
let line = line.expect("could not read input");
number += 1;
let text = line.trim();
if text.is_empty() || text.starts_with('#') {
continue;
}
match parse_line(text) {
Ok((key, value)) => {
settings += 1;
println!("{key} = {value}");
}
Err(e) => {
errors += 1;
println!("line {number}: {e}");
}
}
}
println!("{settings} settings, {errors} errors");
}
# server settings
port = 8080
workers = four
timeout 30
= 5
retries = 3
port = 8080
line 3: bad value for workers: invalid digit found in string
line 5: expected key = value
line 6: missing key
retries = 3
2 settings, 3 errors
The design decisions are the ones you'll make at work:
- Low-level code propagates, high-level code decides.
parse_linedoesn't print or exit; it returns aResultand uses?. Onlymainknows the right policy: report and continue. - Errors carry context.
invalid digit found in stringalone doesn't help anyone.line 3: bad value for workers: invalid digit found in stringsays where and what. Each layer adds what it knows:parse_lineknows the key,mainknows the line number. - Report all problems, or stop at the first? For a config file, listing every bad line at once saves the user several rounds of fix-and-retry. For a sequence of steps where later ones depend on earlier ones (like the ATM), stopping at the first error with
?is right. - No
unwrapon input. The onlyexpectis on reading stdin, which only fails if something is badly wrong with the system.
Common mistakes
- Printing inside low-level functions. A
parse_linethat prints its own errors can't be reused by a program that wants to show them in a web page or a log. Return the error; let the caller present it. - Losing the original error.
.map_err(|_| String::from("bad value"))throws away the reason. Include it:.map_err(|e| format!("bad value: {e}")). - Counting lines after skipping. Users count every line in the file, including comments and blank lines. Increment the line number before any
continue.
Your task
A warehouse imports inventory rows like widget,4,2.50 (name, quantity, price). Write fn parse_item(line: &str) -> Result<Item, String> that splits the trimmed line on commas and checks, in this order:
- not exactly 3 fields:
Err("expected 3 fields, got <n>") - empty name (after trimming):
Err("missing name") - quantity not a valid
u32:Err("bad quantity: <error>") - price not a valid
f64:Err("bad price: <error>")
Trim each field. line.split(',').collect() gathers the pieces into a Vec<&str> (the starter does this for you).
Challenge 2
Now the whole import program. The starter has a working parse_item. Read every line from stdin, counting line numbers from 1 (blank lines count too, but skip them). For each non-blank line:
- on
Ok(item): print<name>: <quantity> x <price with 2 decimals>and addquantity * priceto a total value - on
Err(e): printline <n>: <e>
At the end print total value: <total with 2 decimals>. For the input below the output is:
widget: 4 x 2.50
line 2: bad quantity: invalid digit found in string
gear: 2 x 10.00
total value: 30.00
(input: widget,4,2.50, then bolt,many,0.10, then a blank line, then gear,2,10)
Challenge 3
A sign-up form sends a username, an age and an email as text. Write fn validate_signup(username: &str, age: &str, email: &str) -> Result<Signup, String> (the struct is in the starter). Trim all three inputs, then check in this order, using ? where it helps:
- username shorter than 3 characters:
Err("username too short") - age not a valid
u8:Err("invalid age: <error>") - age under 13:
Err("must be at least 13") - email without an
@:Err("invalid email")
On success return the Signup with the trimmed username and email.