Rust Environment Variables and Processes
Every program runs inside an environment. The operating system hands it a set of named text values, like PATH, HOME, or PORT. These are environment variables. They are a simple way to configure a program without changing its code: the same binary can use port 8080 on your laptop and port 80 on a server.
A program also lives among other programs. It can finish with an exit code that tells the caller “it worked” or “it failed”, and it can start other programs itself. In Rust, the tools for all of this live in two standard modules: std::env and std::process. No crates needed.
Reading a variable with env::var
Section titled “Reading a variable with env::var”std::env::var takes a name and returns a Result. It is Ok(String) when the variable exists, and Err when it is missing (or not valid Unicode). Because a missing variable is a normal situation, Rust makes you handle it:
use std::env;
fn main() { // A name that is almost certainly not set on any machine. match env::var("RUSTTUTORIAL_API_KEY") { Ok(value) => println!("Found it: {value}"), Err(e) => println!("Not available: {e}"), }
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080")); println!("Port: {port}");}Output:
Not available: environment variable not foundPort: 8080Use match when you want to react to the missing case. Use .unwrap_or(default) when a missing variable simply means “use the default”.
Defaults, numbers, and flags
Section titled “Defaults, numbers, and flags”Environment variables are always text. To get a number, you parse it, and parsing can fail too. A common pattern is: turn the Result into an Option with .ok(), try to parse, and fall back to a default if anything goes wrong:
use std::env;
fn main() { let workers: u32 = env::var("RUSTTUTORIAL_WORKERS") .ok() .and_then(|text| text.parse().ok()) .unwrap_or(4);
let debug = env::var("RUSTTUTORIAL_DEBUG").is_ok();
println!("workers = {workers}"); println!("debug = {debug}");}Output:
workers = 4debug = false.is_ok() is a quick way to treat a variable as an on/off flag: if it is set at all, the flag is on.
var_os, vars, and setting variables
Section titled “var_os, vars, and setting variables”env::var_os returns Option<OsString> instead of a Result. It never fails on strange bytes, so it is handy for paths. env::vars() gives you every variable as (name, value) pairs.
env::set_var changes a variable for the current process. Notice the unsafe block around it:
use std::env;
fn main() { unsafe { env::set_var("MYAPP_COLOR", "blue"); env::set_var("MYAPP_SIZE", "large"); }
println!("{:?}", env::var_os("MYAPP_COLOR")); println!("{:?}", env::var_os("MYAPP_MISSING"));
let mut mine: Vec<(String, String)> = env::vars() .filter(|(key, _)| key.starts_with("MYAPP_")) .collect(); mine.sort(); // the order of env::vars() is not guaranteed
for (key, value) in &mine { println!("{key}={value}"); }}Output:
Some("blue")NoneMYAPP_COLOR=blueMYAPP_SIZE=largeWhy is set_var unsafe? On many operating systems, the environment is one shared table for the whole process. If one thread changes it while another thread (or a C library) reads it, the program can crash. Rust can’t check this for you, so since edition 2024 set_var and remove_var are unsafe functions. In edition 2021 they still compile without unsafe, but writing the block anyway works in both. The safe rule: only set variables at the very start of main, before you start any threads. Even better, don’t change your own environment at all. To give a variable to a child program, use Command::env (shown below). See Unsafe Rust for what unsafe means.
The current directory
Section titled “The current directory”env::current_dir() returns the folder the program was started from, as a PathBuf. Relative paths like "config.toml" are resolved against it. The real path depends on your machine, so this example only prints facts that are true everywhere:
use std::env;
fn main() { let dir = env::current_dir().expect("could not read the current directory"); println!("Is the path absolute? {}", dir.is_absolute());
let config = dir.join("config.toml"); println!("Ends with config.toml? {}", config.ends_with("config.toml"));}Output:
Is the path absolute? trueEnds with config.toml? truePrint dir.display() on your own machine to see the full path. Command-line arguments live in std::env too; they have their own page: Command-Line Arguments.
Exit codes with process::exit
Section titled “Exit codes with process::exit”When a program ends, it returns a number to whoever started it. 0 means success; anything else means failure. Shell scripts and CI systems check this number. std::process::exit(code) stops the program right away with that code:
use std::env;use std::process;
fn main() { println!("Starting up...");
let token = match env::var("RUSTTUTORIAL_TOKEN") { Ok(t) => t, Err(_) => { eprintln!("error: RUSTTUTORIAL_TOKEN is not set"); process::exit(1); } };
println!("Using token {token}");}Output:
Starting up...The error line is printed with eprintln!, so it goes to stderr, not stdout. The Playground shows it in a separate area, together with the exit status.
Two things to know. First, process::exit ends the program immediately, so destructors (Drop) for values still alive do not run. Second, you often don’t need it: if main returns Result<(), E> and you return Err, Rust prints the error and exits with code 1 for you.
Running other programs with Command
Section titled “Running other programs with Command”std::process::Command starts another program. You build it step by step: the program name, then each argument with .arg() or .args(). Then you choose how to run it:
.output()waits for it to finish and captures its stdout and stderr..status()waits for it, but lets its output go straight to your terminal.
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("Hello") .arg("from a child process") .output() .expect("failed to run echo");
let text = String::from_utf8_lossy(&output.stdout); println!("success: {}", output.status.success()); println!("stdout: {}", text.trim());}Output:
success: truestdout: Hello from a child processoutput.stdout is raw bytes (Vec<u8>), because a program can print anything. String::from_utf8_lossy turns it into text.
Here is .status() reading an exit code, and .env() passing a variable to the child only:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "exit 3"]) .status() .expect("failed to run sh");
println!("success: {}", status.success()); println!("code: {:?}", status.code());
let output = Command::new("sh") .args(["-c", "echo \"$GREETING, $NAME\""]) .env("GREETING", "Hi") .env("NAME", "Ferris") .output() .expect("failed to run sh"); print!("{}", String::from_utf8_lossy(&output.stdout));}Output:
success: falsecode: Some(3)Hi, FerrisOn Windows: echo is not a separate program there; it is built into the cmd shell, and sh usually doesn’t exist. The same idea looks like Command::new("cmd").args(["/C", "echo Hello"]). The examples above work on Linux, macOS, and the Rust Playground.
Putting it together: config from the environment
Section titled “Putting it together: config from the environment”A real program usually reads all its settings once, at startup, into a struct. Each setting has a sensible default, and a bad value gives a clear message instead of a crash:
use std::env;
#[derive(Debug)]struct Config { host: String, port: u16, verbose: bool,}
impl Config { fn from_env() -> Config { let host = env::var("APP_HOST").unwrap_or_else(|_| String::from("127.0.0.1"));
let port = match env::var("APP_PORT") { Ok(text) => text.parse().unwrap_or_else(|_| { println!("APP_PORT={text:?} is not a number, using 3000"); 3000 }), Err(_) => 3000, };
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose } }}
fn main() { // Simulate what a user might set before running the program. unsafe { env::set_var("APP_PORT", "eighty"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}Output:
APP_PORT="eighty" is not a number, using 3000Config { host: "127.0.0.1", port: 3000, verbose: true }In real use, delete the set_var lines and set the variables in your shell instead:
APP_PORT=9000 APP_VERBOSE=1 cargo run(On Windows PowerShell: $env:APP_PORT="9000"; cargo run.)
Common mistakes
Section titled “Common mistakes”Treating env::var as if it returned a String. It returns a Result, because the variable might not exist:
let port: String = env::var("PORT");error[E0308]: mismatched types --> src/main.rs:4:24 |4 | let port: String = env::var("PORT"); | ------ ^^^^^^^^^^^^^^^^ expected `String`, found `Result<String, VarError>` | | | expected due to thisFix: decide what a missing variable means. env::var("PORT").unwrap_or(String::from("8080")) for a default, or match to report it.
Calling set_var without unsafe in edition 2024. New projects made with cargo new use edition 2024, so this fails:
error[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block --> src/main.rs:2:5 |2 | std::env::set_var("MODE", "fast"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe functionFix: wrap it in unsafe { ... } and call it only before any threads start, or use Command::env for child programs.
Putting the whole command line in Command::new. Command::new("echo hello") looks for a program literally named echo hello, which doesn’t exist. .output() returns Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }). Fix: the program name goes in new, and every argument goes in .arg() or .args(): Command::new("echo").arg("hello").
env::var("NAME")returnsResult<String, VarError>. Handle the missing case withmatch,.unwrap_or(), or.ok().- Environment values are text. Parse numbers yourself and fall back to a default when parsing fails.
env::set_varisunsafein edition 2024 because the environment is shared by all threads. PreferCommand::envfor child programs.process::exit(code)ends the program at once with an exit code:0is success, anything else is failure.Command::new("program").args([...])runs another program..output()captures what it prints;.status()gives you its exit code.
Quick check
Section titled “Quick check”Quick check
1. What does env::var("PORT") return when PORT is not set?
2. Why is env::set_var an unsafe function in edition 2024?
3. Which exit code traditionally means "success"?
4. How do you run ls -l with Command?
Score: 0 / 4
Read more
Section titled “Read more”- std::env — Rust standard library: every function for variables, arguments, and directories.
- Working with Environment Variables — The Rust Book: the Book’s project reads a setting from the environment.
- std::process::Command: all the options for starting programs, with examples for Windows and Unix.
- Newly unsafe functions — Edition Guide: the official explanation of why
set_varbecameunsafe. - Child processes — Rust by Example: more short examples of running programs and using pipes.