Before you begin
Why this matters
When a program runs with the wrong setting, the first question is: where did this value come from? If you can follow the order in which your code looks, you can answer that by reading it, and you can spot the lines that replace what someone typed without reporting an error.
What this helps you reason about
- Predict which place a setting comes from when several disagree.
- Spot a call such as unwrap_or that swaps a bad value for the default.
- Return an error that says which place held the bad value.
A program often needs one setting, such as a port number, that can come from several places. This lesson builds the part of a Rust program that picks one value, checks it, and says where it came from.
Already know this? The short version
- Take the setting from the highest source that is present, here flag, then environment variable, then file, then the default.
- Hold each source as an Option<&str>; Some("") is present, so choose with if let or Option::or, and use env::var(...).ok() to turn the environment into an Option.
- Check only the chosen text, with your own rule (here 1 to 5 ASCII digits, then 1 to 65535), because parse::<u16>() accepts "+80" and "0".
- An invalid winner is an Err that names its source and text; unwrap_or(default) hides it under the winner’s name.
- Read env::args(), the environment, and files in one place, at the start of the program, and pass their text to a function that reads nothing itself.
Words in this lesson (8)
- setting
- One value a program needs before it can do its work, such as the port number it listens on.
- source
- A place a setting can come from: a command-line flag, an environment variable, a configuration file, or a default written in the program.
- precedence
- The fixed order that decides which source wins when several supply the same setting. In this lesson: flag, then environment variable, then file, then default.
- present and absent
- A source is present when it supplies a value for the setting, even an empty one. It is absent when it supplies nothing at all.
- Option
- A type whose value is either Some(x), holding a value, or None, holding nothing. Option<u32>, for example, is a number that may be missing.
- Result
- A type whose value is either Ok(x) for success or Err(e) for failure. Functions that can fail, such as parse, return a Result.
- validation
- Checking that a value follows the rules the program needs, such as “1 to 5 digits, from 1 to 65535”, before using it.
- provenance
- A record of where a value came from: here, the name of the source that won, kept with the value and named in every error.
Build brief · one port setting
- Input
- The text of a flag, an environment variable, and a file line, each of which may be absent.
- Output
- A port number and the source it came from, or an error that names the source and its text.
- Policy
- flag, then env, then file, then the default 3000. In this program’s policy, a source that is set counts even when its value is empty. Only the winner is checked: 1 to 5 digits, 1 to 65535. An invalid winner is an error.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Precedence
Which source wins?
A setting is one value a program needs before it can do its work, such as the port number it listens on. A setting can come from several sources: a command-line flag such as --port=7000, typed after the program’s name; an environment variable such as APP_PORT, set before the program starts; a line in a configuration file; or a default written in the program.
When several sources supply the port, the program needs a rule. Precedence is that rule: a fixed order of sources in which the first one that supplies a value wins. This program uses a common order: flag, then environment variable, then file, then the default 3000.
For now, the sources are parameters of type Option<&str>: Some("7000") when the source supplies text, None when it is absent. if let Some(raw) = flag runs its block only when flag is Some, and names the text raw.
fn choose(flag: Option<&str>, env: Option<&str>, file: Option<&str>) -> String {
if let Some(raw) = flag {
return format!("{} from flag", raw);
}
if let Some(raw) = env {
return format!("{} from env", raw);
}
if let Some(raw) = file {
return format!("{} from file", raw);
}
String::from("3000 from default")
} Look at the sources in precedence order and stop at the first one that supplies a value.
Precisely: the order is a choice
No language or operating system sets this order. Flag, then environment, then file, then default is a common convention, because each source is more specific to one run than the next. Some tools add more sources, such as a file per user and a file per project. Whatever the order, a program should write it down where its users can read it.
Experiment
Follow the sources from the highest
if let Some(raw) = flag matches only when flag holds a value, and binds that value to raw. choose tests the sources in precedence order and returns at the first match, so the lower sources are never looked at.
Why it matters When a setting surprises you, follow the same order by hand. The first source that is present is the one that won.
Predict which source wins before running. Then change Some("5000") in the call to None, so the environment variable is absent too, and predict again.
fn choose(flag: Option<&str>, env: Option<&str>, file: Option<&str>) -> String {
if let Some(raw) = flag {
return format!("{} from flag", raw);
}
if let Some(raw) = env {
return format!("{} from env", raw);
}
if let Some(raw) = file {
return format!("{} from file", raw);
}
String::from("3000 from default")
}
fn main() {
let port = choose(None, Some("5000"), Some("4100"));
println!("{}", port);
}
What to look for
Step into choose. flag shows None, so the first if let does not match and the next stop is the env line. env shows Some("5000" (len=4, data=0x…)), so raw is bound there. raw shows "5000" (len=4, data=0x…): the text, its length in bytes, and the address where the text starts. Back in main, port shows "5000 from env". After the edit, env shows None too, the file line matches, and the run prints 4100 from file.
Expected output
5000 from env02 / Presence
Is an empty value the same as a missing one?
A source is present when it supplies a value, and absent when it supplies nothing. Starting a program with APP_PORT set to "" sets the variable, to an empty value. In this program’s policy, a source that is set counts even when its value is empty: someone set it, so the program checks that empty value and reports an error, instead of moving on to the next source. Some tools choose the other policy and treat an empty variable as not set; either is fine if the documentation says which.
Option keeps the two apart: Some("") is present and empty, None is absent. a.or(b) keeps a when it is Some, even Some(""), and gives b only when a is None. The real environment comes from env::var("APP_PORT"), which returns Ok(text) when the variable is set and Err when it is not; .ok() turns that into an Option.
let env: Option<&str> = Some(""); // set, but empty
let file: Option<&str> = Some("4100");
let chosen = env.or(file); // Some(""): env is present Choose with if let or or on Options. Some("") is a value, not a missing one.
Coming from JavaScript: Option::or behaves like ?? rather than ||: it keeps Some(""), because the empty string is a value.
Precisely: env::var errors
env::var returns Err(VarError::NotPresent) for a missing variable and Err(VarError::NotUnicode) for one whose bytes are not valid UTF-8. .ok() turns both into None. If the second case matters, match on the error, or use env::var_os.
Experiment
Tell an empty value from a missing one
a.or(b) keeps a when a is Some, even Some(""), and gives b only when a is None. Option keeps “is there a value” apart from the value itself, so an empty string cannot pass for a missing one.
Why it matters Hold each source in an Option and choose with or. Check the chosen text afterwards.
Predict the printed value before running. Then change Some("") on the env line to None and predict again.
fn main() {
// Stands in for env::var("APP_PORT").ok(): set, but empty.
let env: Option<&str> = Some("");
let file: Option<&str> = Some("4100");
let chosen = env.or(file);
println!("{:?}", chosen);
}
What to look for
Step past each line. env shows Some("" (len=0, data=0x…)): present, holding the empty string. chosen shows the same, and the run prints Some(""); the file’s 4100 is not used. After the edit, env shows None, chosen shows Some("4100" (len=4, data=0x…)), and the run prints Some("4100").
Expected output
Some("")03 / Validation
What if the winning value is not a port?
The chosen value is still text. Before the program uses it as a port, validation checks the text against the program’s own rule: one to five ASCII digits (the characters 0 to 9), and a number from 1 to 65535. Leading zeros are allowed, so 00080 means 80.
parse::<u16>() is not that rule. It returns Err for "80x" and " 80", but it accepts "+80", and it accepts 0, which is not a usable port. So the program checks the bytes first. raw.bytes().all(|b| b.is_ascii_digit()) is true when every byte is a digit; |b| … is a closure, a small unnamed function with parameter b.
When the winning value fails the check, the program could fall through to the next source or stop with an error. This program stops. raw.parse().unwrap_or(3000) is the common way to get this wrong: it turns every Err into 3000, and the default then appears under the flag’s name.
let digits = !raw.is_empty()
&& raw.len() <= 5
&& raw.bytes().all(|b| b.is_ascii_digit());
if !digits {
return Err(format!("{} {:?} is not 1 to 5 digits", source, raw));
}
match raw.parse::<u16>() {
Ok(port) if port >= 1 => Ok((port, source)),
_ => Err(format!("{} {:?} is not between 1 and 65535", source, raw)),
} Check the value that won. If it fails, return Err; do not try the next source.
Coming from Python: unwrap_or(3000) does what try: int(raw) except ValueError: 3000 does: it hides the error.
Precisely: the match
Ok(port) if port >= 1 is a match arm with a guard: it matches only when the condition is also true. _ matches everything else, which here is 0 and the five-digit numbers above 65535, for which parse::<u16>() returns Err.
Experiment
Give the parser a value that is not a port
parse returns a Result: Err for "80x", and Ok(0) for "0". unwrap_or(3000) turns every Err into 3000, so the error disappears and the default appears under the flag’s name. u16 allows 0, which is not a usable port, so parse alone is not this program’s rule.
Why it matters Handle the Result with match and report Err with the source and the text. Check the range yourself.
Predict the output before running. Then change "80x" to "0" and predict again.
fn port_from(raw: &str) -> u16 {
raw.parse().unwrap_or(3000)
}
fn main() {
let port = port_from("80x");
println!("{} from flag", port);
}
What to look for
The stepper goes into port_from and comes back. In main, port shows 3000, and the run prints 3000 from flag, although the flag did not say 3000. After the edit, port shows 0, and the run prints 0 from flag.
Expected output
3000 from flag04 / Provenance
How does the program say where a value came from?
Provenance is a record of where a value came from. The program keeps it by returning the source with the port, and by putting the source and the raw text in every error. Then error: flag "80x" is not 1 to 5 digits tells the person which of the four places to fix.
load returns Result<(u16, &'static str), String>: Ok with the port and the source’s name, or Err with a message. &'static str is text written in the program itself, such as "flag". {:?} prints the raw text in quotes, so an empty value shows as "".
match load(flag, env, file) {
Ok((port, source)) => println!("{} from {}", port, source),
Err(message) => println!("error: {}", message),
} Return the source with the value, and name the source and the raw text in every error.
Coming from Go: Instead of a value and an error side by side, Rust returns one Result, and the compiler makes you handle Err before you can use the port.
Precisely: showing every setting at start
Many tools can print each setting with the place it came from; git config --list --show-origin is one example. A function that already returns the source makes that a few lines of code.
05 / Real sources
Where do the real flags, variables, and files come in?
A real program reads its sources from outside. env::args() gives the words of the command line, env::var reads an environment variable, and fs::read_to_string reads a file. Each tells you when something is missing.
Keep those reads in one place, at the start of the program. That code collects the text and passes it to load; load itself reads nothing from outside. Then you can call load with any combination of sources, as the experiments and the program on this page do.
The stepper on this page always starts a program without flags, and with an environment you cannot set from outside. So this lesson passes the sources as Options. That is the same shape env::var("APP_PORT").ok() gives a real program.
// The start of a real program; not run on this page.
let mut flag = None;
for arg in std::env::args().skip(1) {
if let Some(value) = arg.strip_prefix("--port=") {
flag = Some(value.to_string());
}
}
let env = std::env::var("APP_PORT").ok();
let result = load(flag.as_deref(), env.as_deref(), None); Read the command line, the environment, and files in one place, and pass plain values to the code that decides.
Precisely: the command line
The first item of env::args() is the program’s own name, so skip(1) keeps only the words after it. as_deref turns an Option<String> into the Option<&str> that load takes. A real tool usually uses a parser crate such as clap. The file is passed as None here; reading one is a separate topic in this module, Files, parsing, and serialization.
06 / Try it
Can you predict the winner and the outcome?
The workbench below is a model of this lesson’s policy that runs in your browser. It does not run Rust code or read your environment. It draws the sources in the reverse of this lesson’s order: flag, the highest source, is at the right end. So the winner is the source furthest to the right that is present.
Predict the result before each change: turn off flag; turn off env as well; turn flag back on and clear its text; then type broken into file while flag holds 7000.
The flag is absent, APP_PORT is set to 0080, and the file says 70000. What does the program report, and why?
Reveal the reasoning
80 from env. The environment variable is the highest source that is present. 0080 is four digits, and leading zeros are allowed, so it gives 80. The file’s 70000 would fail the range check, but the file did not win, so it is never checked.
07 / Whole program
Does the whole program follow the policy?
The complete program below joins the pieces and runs the thirteen acceptance cases, one output line each. Each case gives the flag, the environment variable, and the file, with None for an absent source. Copy it into a file and build it with rustc or cargo to see the same thirteen lines.
fn load(
flag: Option<&str>,
env: Option<&str>,
file: Option<&str>,
) -> Result<(u16, &'static str), String> {
let (source, raw) = if let Some(raw) = flag {
("flag", raw)
} else if let Some(raw) = env {
("env", raw)
} else if let Some(raw) = file {
("file", raw)
} else {
("default", "3000")
};
let digits = !raw.is_empty()
&& raw.len() <= 5
&& raw.bytes().all(|b| b.is_ascii_digit());
if !digits {
return Err(format!("{} {:?} is not 1 to 5 digits", source, raw));
}
match raw.parse::<u16>() {
Ok(port) if port >= 1 => Ok((port, source)),
_ => Err(format!("{} {:?} is not between 1 and 65535", source, raw)),
}
}
fn main() {
// Each case is (flag, env, file); None means absent.
let cases = [
(Some("7000"), Some("5000"), Some("4100")),
(None, Some("5000"), Some("4100")),
(None, None, Some("4100")),
(None, None, None),
(Some(""), Some("5000"), None),
(Some("80x"), Some("5000"), None),
(Some(" 80"), None, None),
(Some("+80"), None, None),
(Some("0"), None, None),
(Some("65536"), None, None),
(Some("7000"), None, Some("broken")),
(Some("00080"), None, None),
(None, Some(""), Some("4100")),
];
for (flag, env, file) in cases {
match load(flag, env, file) {
Ok((port, source)) => println!("{} from {}", port, source),
Err(message) => println!("error: {}", message),
}
}
}
Acceptance cases
- flag 7000, env 5000, file 41007000 from flag
- no flag; env 5000, file 41005000 from env
- only file 41004100 from file
- no flag, env, or file3000 from default
- flag "" (set, but empty), env 5000error: flag "" is not 1 to 5 digits
- flag 80x, env 5000error: flag "80x" is not 1 to 5 digits
- flag " 80" (a space first)error: flag " 80" is not 1 to 5 digits
- flag +80error: flag "+80" is not 1 to 5 digits
- flag 0error: flag "0" is not between 1 and 65535
- flag 65536error: flag "65536" is not between 1 and 65535
- flag 7000, file broken7000 from flag; the file is never checked
- flag 0008080 from flag
- env "" (set, but empty), file 4100error: env "" is not 1 to 5 digits
What does this program settle, and what does it leave out?
Reveal the reasoning
It settles one setting: which source wins, whether its text is a port, and how to say where it came from. It does not read a real command line, environment, or file, check settings that did not win, or start anything on the port. Each of those is a separate piece with its own rules.
Practice tickets
- CFG-101 · Stop hiding a bad retention setting → Free
- CFG-102 · Let the last width win → Sign in · Pro
- CFG-103 · Choose the watering minutes → Sign in · Pro
Keep exploring
I/O, buffering, and external state →