Search code examples
rustenvironment-variableslifetime

Why does &str from env::var not live long enough?


I am trying to return a &str from an environment variable with a default value if it doesn't exist:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let value: &str = if args.len() < 3 {
        match env::var("DEFAULT") {
            Ok(val) => val.as_str(),
            Err(_) => "default",
        }
    } else {
        args[1].as_str()
    };
}

This fails with

error[E0597]: `val` does not live long enough
 --> tmp.rs:8:24
  |
6 |     let value: &str = if args.len() < 3 {
  |         ----- borrow later stored here
7 |         match env::var("DEFAULT") {
8 |             Ok(val) => val.as_str(),
  |                ---     ^^^        - `val` dropped here while still borrowed
  |                |       |
  |                |       borrowed value does not live long enough
  |                binding `val` declared here

How would I fix this? Moving env::var outside the if statement does not help.


Solution

  • The request does not make much sense on its face, because env::var always returns a copy of the actual environment variable (and once that's validated and decoded). val is the owner of that value, there's nobody else keeping it alive for you.

    Hence the message: by not keeping a handle on val, you're letting it go out of scope and be dropped, thus invalidating any reference you would have taken.

    Now there are mutiple ways to handle this:

    1. Just use String, convert default to a String, and pop the relevant value out of the args

      e.g.

       use std::env;
       fn main() {
           let mut args: Vec<_> = env::args().collect();
           let value = if args.len() < 3 {
               match env::var("DEFAULT") {
                   Ok(val) => val,
                   Err(_) => "default".to_string(),
               }
           } else {
               args.remove(1)
           };
       }
      

      That avoids the reference part, no reference, no issue with not keeping the source alive.

      You could even pop the values out of std::env::Args by hand: it's an exact size iterator, so you don't need to collect it to a vec to know how many values it contains, you can ask it upfront.

    2. Use Cow, that lets you unify / abstract over String and &str:

       use std::borrow::Cow;
       use std::env;
       fn main() {
           let args: Vec<_> = env::args().collect();
           let value: Cow<'_, str> = if args.len() < 3 {
               match env::var("DEFAULT") {
                   Ok(val) => val.into(),
                   Err(_) => "default".into(),
               }
           } else {
               args[1].as_str().into()
           };
       }
      

      That works around the reference, by abstracting over owned / reference entirely.

    3. Stow the String in a function-local variable before taking a reference to that:

       use std::env;
       fn main() {
           let env;
      
           let args: Vec<_> = env::args().collect();
           let value = if args.len() < 3 {
               match env::var("DEFAULT") {
                   Ok(val) => {
                       env = val;
                       &env
                   },
                   Err(_) => "default",
               }
           } else {
               &args[1]
           };
       }
      

      That solves the reference by keeping its source alive.