Skip to content

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.

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 found
Port: 8080
▶ Try it Yourself

Use match when you want to react to the missing case. Use .unwrap_or(default) when a missing variable simply means “use the default”.

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 = 4
debug = false
▶ Try it Yourself

.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.

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")
None
MYAPP_COLOR=blue
MYAPP_SIZE=large
▶ Try it Yourself

Why 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.

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? true
Ends with config.toml? true
▶ Try it Yourself

Print 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.

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...
▶ Try it Yourself

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.

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: true
stdout: Hello from a child process
▶ Try it Yourself

output.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: false
code: Some(3)
Hi, Ferris
▶ Try it Yourself

On 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 3000
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ Try it Yourself

In real use, delete the set_var lines and set the variables in your shell instead:

Terminal window
APP_PORT=9000 APP_VERBOSE=1 cargo run

(On Windows PowerShell: $env:APP_PORT="9000"; cargo run.)

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 this

Fix: 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 function

Fix: 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") returns Result<String, VarError>. Handle the missing case with match, .unwrap_or(), or .ok().
  • Environment values are text. Parse numbers yourself and fall back to a default when parsing fails.
  • env::set_var is unsafe in edition 2024 because the environment is shared by all threads. Prefer Command::env for child programs.
  • process::exit(code) ends the program at once with an exit code: 0 is 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

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