Environment variables and config
A program that runs on a server is configured from outside: environment variables for the container, a file for the settings that are too long for a variable. The standard library gives std::env for the first and nothing for the second, so the file is read and parsed like any other text.
This lesson reads a variable with a default, lists the environment, parses a small configuration file, reports a missing value with a status, and ends with the compiler refusing a string literal where the variable is a String.
env::var and a default
env::var returns a Result<String, VarError>, because a variable can be missing or not valid Unicode. unwrap_or_else with a closure is the form that builds a default only when the lookup failed.
The program reads APP_MODE and prints it. Without the variable the default is used, and with it the value wins.
use std::env;
fn main() {
let mode = env::var("APP_MODE").unwrap_or_else(|_| String::from("dev"));
println!("mode {mode}");
}
rustc --edition 2024 var.rs
./var
APP_MODE=release ./var
mode dev
mode release
The prefix on the command line is the smallest way to set a variable for one run, and it is how a test controls the configuration without touching the machine. The previous lesson is the other input channel.
A process can also change its own environment with env::set_var, which edition 2024 marks unsafe: another thread may be reading the environment at the same moment, and the C library underneath is not thread safe for that call.
The whole environment
env::vars yields the pairs as an iterator, and env::var is a lookup with the same information. The iterator is what a program uses to pass its environment to a child process, and the lookup is what it uses for its own settings.
The program asks two questions that a shell always answers: does a PATH exist, and can it be read as a string. Both are true on every system this course runs on.
use std::env;
fn main() {
println!("{}", env::vars().any(|(key, _)| key == "PATH"));
println!("{}", env::var("PATH").is_ok());
}
rustc --edition 2024 vars.rs
./vars
true
true
Printing the values themselves would print a machine-specific string, which is why the program prints the answers instead. A program that needs the whole environment sorts it first, because the order of the iterator is not defined.
A configuration file
A file of key = value lines is parsed with lines, split_once and a match over the keys. let ... else skips the lines without a separator, and every value keeps its default when it is missing or wrong.
The program reads /tmp/app.ini and prints the host and the port. The file on this machine holds example.org and 9000.
use std::fs;
fn main() {
let text = fs::read_to_string("/tmp/app.ini").unwrap();
let mut host = String::from("localhost");
let mut port: u16 = 8080;
for line in text.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key.trim() {
"host" => host = value.trim().to_string(),
"port" => port = value.trim().parse().unwrap_or(port),
_ => {}
}
}
println!("{host}:{port}");
}
printf 'host = example.org\nport = 9000\n' > /tmp/app.ini
rustc --edition 2024 config.rs
./config
example.org:9000
The defaults are what a missing line means, and the match ignores a key it does not know, which lets a newer configuration file run on an older program. The lesson on parsing text is the same shape for a line of numbers.
A missing value reported
A setting that has no safe default is a failure, and the program should say which variable is missing before it exits. VarError prints as environment variable not found, which is the sentence to put in the message.
The program requires DATABASE_URL and prints it when it exists. Without the variable it writes the reason to the standard error and returns status two.
use std::env;
use std::process::ExitCode;
fn main() -> ExitCode {
match env::var("DATABASE_URL") {
Ok(value) => {
println!("{value}");
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("DATABASE_URL: {error}");
ExitCode::from(2)
}
}
}
rustc --edition 2024 missing.rs
env -u DATABASE_URL ./missing
echo "status: $?"
DATABASE_URL=postgres://demo ./missing
DATABASE_URL: environment variable not found
status: 2
postgres://demo
env -u removes a variable for one command, which is how a test makes sure the configuration is really read. The message names the variable and the reason, and the status tells the supervisor not to restart the program in a loop. The lesson on error types is the next step when several settings are checked at once.
A string default for a String
unwrap_or takes the value of the same type as the success case, and env::var succeeds with a String. A &str literal therefore does not fit, and the compiler reports E0308 with the two types.
The program passes the literal to unwrap_or. The compiler suggests the conversion that makes the default a String.
use std::env;
fn main() {
let mode = env::var("APP_MODE").unwrap_or("dev");
println!("{mode}");
}
rustc --edition 2024 default_err.rs
error[E0308]: mismatched types
--> default_err.rs:4:47
|
4 | let mode = env::var("APP_MODE").unwrap_or("dev");
| --------- ^^^^^ expected `String`, found `&str`
| |
| arguments to this method are incorrect
|
help: the return type of this call is `&'static str` due to the type of the argument passed
--> default_err.rs:4:16
|
4 | let mode = env::var("APP_MODE").unwrap_or("dev");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: try using a conversion method
|
4 | let mode = env::var("APP_MODE").unwrap_or("dev".to_string());
| +++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
The help names the conversion, and a program that only prints the value can also bind env::var("APP_MODE").as_deref().unwrap_or("dev"), which borrows instead of allocating. Both are correct, and the first is the one the compiler offers. The next lesson uses the host and the port it reads.
Exercises
- Read a LOG_LEVEL variable with the default info and reject any value other than debug, info or warn.
- Add a [server] section to the configuration file and skip the lines of the other sections.
- Print every variable whose name starts with APP_, sorted by name.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
env::varreturn?Answer
A Result with a String or a VarError
A variable can be missing or not valid Unicode, so the call reports both.
-
Why use
unwrap_or_elsewith a closure for a default?Answer
The default is built only when the lookup failed
The closure is lazy, so the allocation happens on the error path only.
-
What does the
VarErrorfor a missing variable print as?Answer
environment variable not found
The Display implementation writes that sentence, and the debug form is the other one.
-
Which status does a configuration failure usually return?
Answer
A non-zero status such as 2
A supervisor checks the status, and zero would look like success.
-
What does this program print when
APP_MODEis not set?use std::env; fn main() { let mode = env::var("APP_MODE").unwrap_or_else(|_| String::from("dev")); println!("{mode}"); }Answer
dev
The closure supplies the default and the value is printed.
-
What does this program print?
use std::env; fn main() { println!("{}", env::var("APP_MODE").is_err()); }Answer
true
The Result is an error for a missing variable, and the program runs without one.
-
What does this program print?
fn main() { let line = "host = example.org"; let (key, value) = line.split_once('=').unwrap(); println!("{} {}", key.trim(), value.trim()); }Answer
host example.org
The two halves are trimmed and printed with one space between them.
-
What does this program print?
fn main() { let text = "port = 9000"; let port: u16 = text.split_once('=').unwrap().1.trim().parse().unwrap_or(8080); println!("{port}"); }Answer
9000
The value parses and the default is never used.
-
What is the first line of the compiler output for this program?
use std::env; fn main() { let mode = env::var("APP_MODE").unwrap_or("dev"); println!("{mode}"); }Answer
error[E0308]: mismatched types
The success value is a String and the argument is a string slice.
-
What does this program print?
fn main() { let value: Option<u16> = "port".parse().ok(); println!("{}", value.unwrap_or(8080)); }Answer
8080
The parse fails, so the default port is printed.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev