Rust makes working with environment variables fairly easy. The standard library provides fundamental APIs as part of std::env which we use to access environment variables inside of our application. If you’re expecting valid unicode values, you can use env::var - which returns an Result<String, VarError> - as shown here:

use std::env;

fn verbose() {
    let name = "USER";
    match env::var(name) {
        Ok(v) => println!("{}: {}", name, v),
        Err(e) => panic!("${} is not set ({})", name, e)
    }
}

fn short() {
    let v = env::var("USER").expect("$USER is not set");
}

In contrast, env::var_os does not check if the value of the desired environment variable is valid unicode. var_os has also a slightly different function signature, it returns an Option<OsString>:

use std::env;

fn main() {
    let u = match env::var_os("USER") {
        Some(v) => v.into_string().unwrap(),
        None => panic!("$USER is not set")
    };
    println!("Got username: {}", u);
}

Alternatively, we can use the built-in macros env! and option_env! to read a particular environment variable. Both macros inspect an environment variable at compile-time.

fn main() {
    let shell = env!("SHELL", "$SHELL is not set");
    println!("Shell is set to {}", shell);
    // output: Shell is set to /use/zsh
    
    let lorem = env!("LOREM_IPSUM");
    // error: environment variable `LOREM_IPSUM` not defined

    let lorem = env!("LOREM_IPSUM", "Please set LOREM_IPSUM");
    // error: Please set `LOREM_IPSUM`
}

If you’re uncertain if the desired environment variable exists at compile time, use option_env! instead. In contrast to env! it’ll never emit an error

fn read_env_var_using_option_env_macro() -> String {
    let optional_value = option_env!("SHELL");
    println!("Shell is set to {:?}", shell);

    return optional_value
        .unwrap_or("no shell set")
        .to_string();
}

Read all environment variables in Rust

You can also read all environment variables by using env::vars() as shown here:

use std::env;

fn main() {
    for (n,v) in env::vars() {
        println!("{}: {}", n,v);
    }
}

Again, env::vars() will panic if any key or value is not valid unicode. If you’re expecting to find non-unicode keys or values use env::vars_os() as shown here:

use std::env;

fn main() {
    for (n,v) in env::vars_os() {
        println!("{}: {}", n.into_string().unwrap(), v.into_string().unwrap());
    }
}

Set an environment variable in Rust

You can set an environment variable (for the scope of the currently running process) using the standard library too. To do so, we use env::set_var(key, value).

use std::env;

fn main() {
    let key = "MYAPP_DEBUG";
    env::set_var(key, "1");
}

The set_var function panics if key is empty, key contains the equal sign =, key contains the NUL character \0, or when the value contains the NUL character \0.

Environment variables with envy

While learning Rust, the functionality provided by the standard library was quite satisfying. However, to increase productivity, I started looking for a more powerful solution. That was when I stumbled upon envy.

Envy can deserialize environment variables into typesafe structs.

You can install envy, by adding it as a dependency to your Cargo.toml file:

[dependencies]
envy = "0.4"
serde = { version = "1.0", features = ["derive"] }

First, let’s start with our configuration struct:

use serde::Deserialize;

fn main() {
    let c = envy::from_env::<Configuration>()
        .expect("Please provide PORT and ITEMS_PER_PAGE env var");

    println!("{:#?}", c)
}


#[derive(Deserialize, Debug)]
struct Configuration {
    port: u16,
    items_per_page: u16
}

To deserialize environment variables into the Configuration struct, use envy::from_env - like shown here as part of the main function:

fn main() {
    let c = envy::from_env::<Configuration>()
        .expect("Please provide PORT and ITEMS_PER_PAGE env vars");
    
    println!("{:#?}", c)
}

You can also “namespace” your environment variables, by taking only environment variables having a certain prefix into consideration:

fn main() {
    let c = envy::prefixed("MY_APP__")
        .from_env::<Configuration>()
        .expect("Please provide MY_APP__PORT and MY_APP__ITEMS_PER_PAGE env vars");
    
    println!("{:#?}", c)
}

It’s also quite useful to provide proper default values for your configuration data. Having a good set of default configuration values, makes you app more robust. To achieve this, we can use the serde attribute, and link to a custom function specifying the default value for a particular member:

#[derive(Deserialize, Debug)]

struct Configuration {
    #[serde(default="default_items_per_page")]
    items_per_page: u16, 
    #[serde(default="default_port")]
    port: u16
}

fn default_items_per_page() -> u16 {
    50
}

fn default_port() -> u16 { 
    8080
} 

Conclusion

Working with environment variables is mission-critical, and it doesn’t matter which programming language you’re using. Environment variables are available on any operating system and you should take them always into context when loading configuration data from outside of your application. Both, the Rust standard library and crates like envy make loading data from environment variables frictionless.