Build a Modern API with Slim 4 - Verify your .env file
Tags: tutorial, api, php, bash
Published: 2023-11-07 15:53:49

Welcome back to this series, it was put on hold some time ago, but I'm now back and ready to go!

In the second part of this series we set up our `.env` file which contained both environment-specific and secret variables, both of which are reasons to have Git ignore the file.

So, how do I know, then, that when I deploy onto my staging or production servers, that the file contains everything I need? During this project there have been a few times I've made some updates, added an environment variable, then realised I didn't make that same change on the server. This is where a verification script comes in.

Keep in mind that this file *is* to be committed, because, whilst it contains the names of all our environment variables, it does not contain their values.

I have also opted for something a little bit different and done this in Bash, this allows complete agnosticism as regards programming languages, so you could be writing your API in PHP, Go, C#, JavaScript, it doesn't matter. As long as your server has Bash, this will run (and considering that both Linux and macOS have Bash baked-in, and I'm _fairly_ sure but not certain that you can run Bash scripts in Windows Powershell, you're more likely to have it than not).

So, how do we get started?

The first and most important line is the hashbang or shebang, also known as

```bash
#!/bin/bash
```

This is what makes the script executable just by running `./check_env.sh` and telling the system that it needs to use the Bash interpreter to run it.

And now we tell it which environment variables should appear in our file:

```bash
#!/bin/bash

required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")
```

And now let's tell it where to find our `.env` file, this will be in the same directory as our script file so simply

```bash
#!/bin/bash

required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")

env_file=".env"
```

However, what if our `.env` file is somewhere different, like in `/shared`? Luckily we can check for that as well:

```bash
if [ $# -eq 1 ]; then
  env_file="$1"
fi
```

###### So, um, what does that do?

This looks for the number of command line arguments(`$#`), and if it equals (`-eq`) 1, set the `env_file` variable to the first argument (`"$1"`), rather than `.env`. So now if we run `./check_env.sh` it will look for our `.env` file locally, whereas if we run `./check_env.sh /shared/bookstore_api/.env` then it will look for it in `/shared/bookstore_api/.env` instead.

`fi` being `if` backwards, just ends the if statement.

Now we need to make sure our `.env` file actually exists.

```bash
if [ ! -f "$env_file" ]; then
  echo "Error: .env file not found."
  exit 1
fi
```

The if statement here is now looking for the file, signified by the `-f` flag before `"$env_file"`, with the preceding `!` meaning negation i.e. if the file is *not* found, do this. It will then echo the error message then completely exit the program. But what does that 1 mean?

This is a concept that carries on strong in low-level programming from the very earliest days of computing. Every program ever written anywhere, ever, has to exit or end eventually, and exit codes are how we indicate whether or not it was successful.

A 0 means success - if it helps, consider it to mean "nothing to report", and anything other than 0 means an error. Some programs will use different numbers for different errors, but for the purpose of this short script, `exit 1` on an error will do just fine.

```bash
#!/bin/bash

# What variables are we looking for?
required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")

# Where is our .env file?
env_file=".env"

# Have we been told it's somewhere else?
if [ $# -eq 1 ]; then
  env_file="$1"
fi

# Is it where it should be?
if [ ! -f "$env_file" ]; then
  echo "Error: .env file not found."
  exit 1
fi
```
Okay so now let's start looking for our variables:

```bash
for var in "${required_env_vars[@]}"; do
  grep -q "^$var=" "$env_file"
  if [ $? -ne 0 ]; then
    echo "Error: $var is missing in the $env_file file."
    exit 1
  fi
done
```

###### Okay, so...what? What's happening here?

Okay so let's break it down:

```bash
for var in "${required_env_vars[@]}"; do
  # Do some stuff
done
```

So this should be a fairly self-explanatory for loop: for each of the required variables defined above, do some stuff. That `[@]` is unique to Bash and means that it treats everything in the array as an item, rather than treating the whole array as an item.

```bash
  grep -q "^$var=" "$env_file"
```

The first line inside the loop, this uses `grep` to search for a line within our `.env` file that matches `^$var=` which means that it contains our variable name, followed immediately by `=`, and the `^` signifies that the line must start with that. Finally, the `-q` means quiet, and tells `grep` not to output anything, just to store its exit code, which will be 0 i.e. success if it finds the line.

```bash
  if [ $? -ne 0 ]; then
    echo "Error: $var is missing in the $env_file file."
    exit 1
  fi
```

That exit code is stored in `$?` and `-ne` means not equal, so if `grep`'s exit code is not 0, then echo the error message and, again, `exit 1`.

Now, finally, if that loop runs fine and we reach the end of it, we can echo a success message and `exit 0`

```bash
echo "All environment variables present and correct in $env_file."

exit 0
```

###### Is there anything else we can add?

Well, yes, I am not by any means an expert on Bash scripting, even just writing this article I can see areas for improvement and I'm sure you can as well! There is something that we can add though and that I will demonstrate below, verification of the values.

Obviously, be careful here, you don't want to commit a file that says something like "the database password should match SuperSecretPassword123", because that will negate the entire point of keeping the password in a secret environment file!

What I will demonstrate though is limiting the options for `_ENVIRONMENT`, which can serve as protection against things like typos.

```bash
ENVIRONMENT=$(grep "^_ENVIRONMENT=" "$env_file" | cut -d'=' -f2)
allowed_environments=("development" "staging" "demo" "production")

if ! [[ " ${allowed_environments[@]} " =~ " $ENVIRONMENT " ]]; then
  echo "Error: _ENVIRONMENT must be one of ${allowed_environments[@]}"
  exit 1
fi
```

So, what's happening here?

We are fetching the provided `_ENVIRONMENT` value by running `grep "^_ENVIRONMENT=" "$env_file"` and piping the resulting line into `cut -d'=' -f2` meaning "cut this line using `=` as the delimiter, and return the second part". Then setting the allowed environments as we did the required variables at the start.

Now, we need to make sure that `ENVIRONMENT` is actually one of our `allowed_environments`, we take care of this in the if statement, then once again, if needed, we echo our error message and `exit 1`.

All of this will leave us with a script that looks like the below:

```bash
#!/bin/bash

required_env_vars=("_ENVIRONMENT" "_PDO_HOST" "_PDO_USERNAME" "_PDO_PASSWORD" "_PDO_NAME" "_ENCRYPTION_CIPHER_METHOD" "_HASHING_COST" "_SMTP_HOST" "_SMTP_USERNAME" "_SMTP_PASSWORD" "_RATE_LIMIT_GET" "_RATE_LIMIT_POST" "_RATE_LIMIT_PUT" "_RATE_LIMIT_DELETE")

env_file=".env"

if [ $# -eq 1 ]; then
  env_file="$1"
fi

if [ ! -f "$env_file" ]; then
  echo "Error: .env file not found."
  exit 1
fi

for var in "${required_env_vars[@]}"; do
  grep -q "^$var=" "$env_file"
  if [ $? -ne 0 ]; then
    echo "Error: $var is missing in the $env_file file."
    exit 1
  fi
done

ENVIRONMENT=$(grep "^_ENVIRONMENT=" "$env_file" | cut -d'=' -f2)
allowed_environments=("development" "staging" "demo" "production")

if ! [[ " ${allowed_environments[@]} " =~ " $ENVIRONMENT " ]]; then
  echo "Error: _ENVIRONMENT must be one of ${allowed_environments[@]}"
  exit 1
fi

echo "All environment variables present and correct in $env_file."

exit 0
```