Banging Your Head Against a Wall: Environment Variables

A tale of a frustrated software developer


What I Was Doing

I was trying to run specs via `RAILS_ENV=test rspec`.  Pretty straight forward, right?

The Issue

The rails app I was working on uses webpacker to compile assets.  When running in a test environment, these assets are compiled to a "public/packs-test" directory. The "packs-test" directory is required in order to run the specs, so if it is missing, things go 💥!  Well, this `packs-test` folder was not being created by webpacker, so my specs were all failing.  😭

The Order of the Environment Variables

Before we dive into what I discoverd.  Let's review the order of precedence for environment variables.

Things are of course a little different between Windows, OSX and Linux environments.  The list below is based on OSX/Linux, but I did want to mention a bit more on Windows Environment Variables.

Windows

Windows has a slight different order of environment variables:

  • System: All Users
  • User: Specific Users
  • Process: Specific Process
  • Volatile: Current logon session only

Let's just consider all of these to be "System" in the order section below for simplicity.

The Order

This is how I imaged the order of environment variables.  Essentially, "Command Line" will override "Zsh/Bash Shell Defaults" which will in turn override "System" level environment variables.

1) System

  • These are the system default environment variables, like "PATH"

2) Shell Defaults

  • Set in "~/.bash_profile" or "~/.zshrc"...etc
  • Set by adding `export FOO_BAR=test` to your file.
  • Loaded when you start up your terminal.
  • Will persist in all shells, similar to system level.

3) Shell Session

  • Set in command line.
  • Set by typing `export FOO_BAR=test` in your terminal.
  • Will persist in current shell and subsequent commands will inherit value.

4) Environment Files

  • Set in `.env`, `.env.test`, etc.
  • Set by adding `FOO_BAR=test` to your .env file.
  • Requires 3rd party load .env files, e.g. "dotenv" packages for rails, javascript
  • Will override system, bash profile and shell session level environment variables.

5) Command Line

  • Example: `FOO_BAR=test yarn start`
  • Will persist in current command only

Tip: To view all your environment variables you can use `set` or `printenv` (osx/linux only)

The Order Lies!

Turns out, the way I was thinking about environment variables was wrong.

Although in most scenarios this order is true because of the typical sequence of operations, for example:

1) System environment variables are loaded by default when you start up your computer.

2) Shell defaults are loaded when you start your shell

3) Environment file overrides are loaded when you start your app

4) Environment variable is set via command, e.g. `RAILS_ENV=test rspec`

So, rather than thinking of environment variables as hierarchical order, a more accurate way of thinking about environment variables is that the last one set is the one used.  It could be from the command line or from your bash profile or from setting it in your shell.

To demonstrate, let's consider that "FOO_BAR" is a standard environment variable defaulted to "BAZ" on every laptop.

```language-bash
❯ echo $FOO_BAR
BAZ
```

Overriding with Shell Session

```language-bash
❯ export FOO_BAR=1

❯ echo $FOO_BAR

1
```

Overriding with Command Line

```language-bash
❯ FOO_BAR=2 && echo $FOO_BAR

❯ echo $FOO_BAR

2
```

Overriding with Shell Session with Shell Defaults

Let's set the value in our shell via:

```language-bash
❯ export FOO_BAR=1

❯ echo $FOO_BAR

1
```

Now, if we were to add `export FOO_BAR=3` to our `.zshrc`, nothing would change in our current shell.

```language-bash
❯ echo $FOO_BAR

1
```

Cool. But, if we were to resource our shell, we'd lose our shell session and it would be set back to what is in the `.zshrc`.

```language-bash
❯ source ~/.zshrc

❯ echo $FOO_BAR

3
```


So, now that we have a fairly good understanding of how environment variables are set, let's dive into what happened to me.

What was actually wrong?

I had a `.env.test` file with `RAILS_ENV=` in it, which was overriding my `RAILS_ENV=test` passed in via command line and subsequently `RAILS_ENV` was being defaulted to `production` because it was an empty string.

This is obviously not what I expected and did not even consider it because in my head I had already ruled it out.  Shouldn't the `RAILS_ENV` that I pass into the command line always override? _Answer: nope!_

Turns out, the heart of the issue was that I did not realize how we were using the dotenv gem.  When using the dotenv gem, you can load `.env` files via `overload` or `load`.  The major difference between these two is that `overload` will override any existing environment variables set!  Yes, you guessed it...we were using the `overload` method in the `test` environment, which in turn was overriding what I was passing in to the command line! 😳


To fix, I just removed `RAILS_ENV` from the `.env.test` file and then everything worked. 🎉


Key Takeaways

When you are hitting a wall...

Environment Variables Can Be Trixy

Environment variables can be loaded in a myriad of ways and sometimes in unexpected ways. Be aware of the libraries you are using to load your environment variables especially.

Be Aware of Your Assumptions

Keep an open mind and question your assumptions.  It can be hard to even be aware of them, so do what you can to get out of the 🤬 headspace.

Step Away

Taking a short break and stepping away can help clear your head. By doing so, you will be able to be more aware of your assumptions and potentially attack your problem from an angle you did not consider.

Never Give Up

This has been the single-most important thing I have adopted.  No matter how frustrated you are or how hopeless you feel, do not give up.

More often than not you are just at the crest of the hill, just keep going!