Skip to content

ReadEnvSecrets

[Source]

Read a .env file and the process environment into an EnvSecrets.

A path with no file at it is not an error: the result is an EnvSecrets holding the environment alone. A file that is there but cannot be read, or that has a malformed line, is a DotEnvError, and neither is safe to carry on from.

Call this once at startup. It reads the file synchronously, so the calling actor's scheduler thread waits for the disk.

Reading the file needs FileRead and FileStat on dotenv. A relative path resolves against the working directory, so pass an absolute path when that directory is not trusted. A file larger than 1 MiB is not read.

An environment variable overrides the .env file. When a name is set in both, the value from vars wins and the file's value is discarded, so the file supplies only the names the environment leaves unset. A .env file is a development convenience; that ordering keeps one from shadowing a secret a deployment supplies. Where one name is set twice in the same source, the first of them wins, which is what getenv returns for the environment.

Names are matched exactly. On Windows, where the environment's own names are case-insensitive, a .env name that differs from an environment name only in case is a second name and is not overridden.

Each line of the file is one of:

  • blank, or a comment beginning with #, both of which are ignored
  • NAME=VALUE, split at the first =

A name is one or more letters, digits and underscores, not starting with a digit. Anything else is malformed, including the export NAME=VALUE that a shell would accept.

Surrounding whitespace is removed from the name and from the value. A value wrapped in a matching pair of " or ' has those quotes removed and keeps the whitespace inside them. Everything else is part of the value: there is no escaping, no ${...} substitution, no value spanning two lines, and no comment after a value, so A=b # note binds b # note.

An environment variable's value is taken verbatim instead, so the same text can mean different things depending on which source supplied it. A=hunter2 keeps its trailing space from the environment and loses it from the file.

Nothing is written back into the process environment. A name read from the file is visible through the returned EnvSecrets and nowhere else, so it does not reach a subprocess.

use "files"
use "sensitive"

actor Main
  new create(env: Env) =>
    let dotenv = FilePath(
      FileAuth(env.root),
      ".env",
      recover val FileCaps .> set(FileRead) .> set(FileStat) end)

    let secrets =
      match ReadEnvSecrets(env.vars, dotenv)
      | let s: EnvSecrets => s
      | let e: DotEnvError =>
        env.err.print(e.string())
        env.exitcode(1)
        return
      end

    try
      let password = secrets("DB_PASSWORD")?
      env.out.print("The password is " + password.string())
    else
      env.out.print("DB_PASSWORD is not set")
    end
primitive val ReadEnvSecrets

Constructors

create

[Source]

new val create()
: ReadEnvSecrets val^

Returns


Public Functions

apply

[Source]

Read dotenv and vars into an EnvSecrets, or report why the file could not be used.

fun box apply(
  vars: Array[String val] val,
  dotenv: FilePath val)
: (EnvSecrets val | DotEnvError)

Parameters

Returns


eq

[Source]

fun box eq(
  that: ReadEnvSecrets val)
: Bool val

Parameters

Returns


ne

[Source]

fun box ne(
  that: ReadEnvSecrets val)
: Bool val

Parameters

Returns