# env

> Read configuration from environment variables and .env files, with typed reads, required settings and defaults.

This is the Markdown version of https://osl-lang.org/packages/env. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:env"
```

`std:env` reads and sets environment variables and works with `.env` files. Use it for configuration that changes between machines: ports, API keys, database paths, a development or production mode. Secrets in the environment stay out of your source and your repository. The examples set their own variables; in a real program they come from the shell, a service manager or a `.env` file.

## Read a variable

`env.get(name)` returns the value as a string, or an empty string when it is unset. Give a second argument for a default. `env.has(name)` tells a variable that is set from one that isn't, even when it is set to an empty value:

```osl
import "std:env"

env.set("DOCS_NAME", "ada").unwrap()

log env.get("DOCS_NAME")
log "[" ++ env.get("DOCS_UNSET") ++ "]"
log env.get("DOCS_UNSET", "fallback")
log env.has("DOCS_NAME") env.has("DOCS_UNSET")

env.unset("DOCS_NAME").unwrap()
log env.has("DOCS_NAME")
```

Output:

```text
ada
[]
fallback
true false
false
```

`env.set` and `env.unset` change the variable for this program and the programs it starts. They don't change your shell. `set` returns a result because the system can refuse a name it doesn't allow.

## Typed reads

Environment variables are text. `env.int`, `env.number` and `env.bool` convert and take a fallback for an unset variable. A value that is set but isn't the right type is an error, which is better than silently using a default when someone mistyped a port:

```osl
import "std:env"

env.set("DOCS_PORT", "8080").unwrap()
env.set("DOCS_RATIO", "0.75").unwrap()
env.set("DOCS_DEBUG", "yes").unwrap()
env.set("DOCS_BAD", "eighty").unwrap()

log env.int("DOCS_PORT", 3000).unwrap()
log env.int("DOCS_UNSET", 3000).unwrap()
log env.number("DOCS_RATIO", 1).unwrap()
log env.bool("DOCS_DEBUG", false).unwrap()

auto bad = env.int("DOCS_BAD", 3000)
log bad.isErr() bad.unwrapErr().kind
```

Output:

```text
8080
3000
0.75
true
true ParseError
```

Booleans accept `1`, `true`, `t`, `yes`, `y` and `on` as true, and `0`, `false`, `f`, `no`, `n` and `off` as false, in any case.

## Required settings

`env.require(name)` returns the value, or an error naming the missing variable. `env.required(...)` checks several at once, and `env.missing(...)` lists the ones that are absent, so a program can report every problem in one go and stop at the start instead of failing halfway:

```osl
import "std:env"

env.set("DOCS_DB", "app.db").unwrap()

log env.require("DOCS_DB").unwrap()
log env.require("DOCS_TOKEN").unwrapErr().message
log env.missing("DOCS_DB", "DOCS_TOKEN", "DOCS_SECRET")

switch env.required("DOCS_DB", "DOCS_TOKEN") (
  case ok(_)
    log "configured"
  case err(e)
    log "cannot start:" e.message
)
```

Output:

```text
app.db
env.require: DOCS_TOKEN is not set
["DOCS_TOKEN","DOCS_SECRET"]
cannot start: env.required: DOCS_TOKEN is not set
```

A variable set to an empty string counts as not set for `require`, `required` and `missing`.

## Expanding references

`env.expand(text)` replaces `$NAME` and `${NAME}` with the variables' values, which is handy for paths from a config file:

```osl
import "std:env"

env.set("DOCS_HOME", "/srv/app").unwrap()
env.set("DOCS_PORT", "8080").unwrap()

log env.expand("$DOCS_HOME/data on port ${DOCS_PORT}")
```

Output:

```text
/srv/app/data on port 8080
```

## Parsing .env text

A `.env` file holds `NAME=value` lines, with comments after `#`, optional quotes and an optional `export`. `env.parse(text)` reads that text into a file handle without touching the real environment:

```osl
import "std:env"

string text = "# database\nHOST=localhost\nNAME=\"my app\"\nURL=$HOST/db\nexport PORT=5432\n"
auto config = env.parse(text)

log config.keys()
log config.get("NAME")
log config.int("PORT", 0).unwrap()
log config.expand("URL")
log config.get("MISSING", "none")
log config.all()
```

Output:

```text
["HOST","NAME","PORT","URL"]
my app
5432
localhost/db
none
{"HOST":"localhost","NAME":"my app","PORT":"5432","URL":"$HOST/db"}
```

A file handle has the same reads as the package (`get`, `int`, `number`, `bool`, `has`, `keys`, `expand`). Values are kept as written, and `expand(key)` resolves references to other values in the file first, then to the environment.

## Load a .env file

`env.load(path)` reads a `.env` file and sets its values as environment variables. It leaves alone variables that are already set, so a real environment wins over the file. With no path it reads `.env`:

```osl
import "std:env"
import "std:fs"

fs.writeFile(".env.demo", "DOCS_GREETING=hello\nDOCS_LEVEL=3\n").unwrap()

env.unset("DOCS_GREETING").unwrap()
env.set("DOCS_LEVEL", "9").unwrap()
env.load(".env.demo").unwrap()

log env.get("DOCS_GREETING")
log env.get("DOCS_LEVEL")

log env.load("does-not-exist.env").unwrapErr().kind
fs.remove(".env.demo", {force: true}).unwrap()
```

Output:

```text
hello
9
NotFoundError
```

`DOCS_LEVEL` kept its value of 9. To make the file win, parse it and call `apply({override: true})`:

```osl
import "std:env"

env.set("DOCS_MODE", "old").unwrap()
auto file = env.parse("DOCS_MODE=new")

file.apply().unwrap()
log env.get("DOCS_MODE")
file.apply({override: true}).unwrap()
log env.get("DOCS_MODE")
```

Output:

```text
old
new
```

Never commit a `.env` file that holds secrets. Commit a `.env.example` with the names and no values.

## Editing and saving a .env file

A file handle can be changed and written back. `set`, `unset` and `merge` return the handle, so calls chain, and `save()` writes to the handle's path:

```osl
import "std:env"
import "std:fs"

fs.remove(".env.edit", {force: true}).unwrap()

auto file = env.parse("A=1\nB=2\n")
file.set("C", 3).merge({D: "four words"}).unset("A")
log file.text()

file.setPath(".env.edit")
file.save().unwrap()

auto again = env.parseFile(".env.edit").unwrap()
log again.keys()
log again.path()
fs.remove(".env.edit", {force: true}).unwrap()
```

Output:

```text
B=2
C=3
D="four words"

["B","C","D"]
.env.edit
```

`env.stringify(object)` formats an object as `.env` text without a handle.

## Modes

`env.mode()` reports the app's mode from `APP_ENV`, `OSL_ENV` or `NODE_ENV`, in that order, and is `development` when none is set. `isDev()`, `isProd()` and `isTest()` test it, which lets a program turn on debug output or strict settings without its own convention:

```osl
import "std:env"

env.set("APP_ENV", "production").unwrap()
log env.mode()
log env.isProd() env.isDev() env.isTest()
```

Output:

```text
production
true false false
```

**Quiz.** `DOCS_PORT` is set to `eighty`. What does `env.int("DOCS_PORT", 3000)` give?

- [ ] `3000`, the fallback.
- [x] A failed result with a `ParseError`.
- [ ] `0`.

Answer: The fallback is for a variable that is unset. A value that is set but wrong is an error you should see.

### Exercise: Port from the environment

Print the port the program would use: the `EX_PORT` variable as an integer, or 8080 when unset. Then set it to `9000` and print again, then to `abc` and print 8080 for the bad value too.

Starting code:

```osl
import "std:env"

env.unset("EX_PORT").unwrap()
log 0
```

It should print:

```text
8080
9000
8080
```

Hint 1: `env.int("EX_PORT", 8080)` returns a result.

Hint 2: `env.set("EX_PORT", "9000").unwrap()` between reads, and `.unwrapOr(8080)` covers the bad value.

A solution:

```osl
import "std:env"

env.unset("EX_PORT").unwrap()
log env.int("EX_PORT", 8080).unwrapOr(8080)
env.set("EX_PORT", "9000").unwrap()
log env.int("EX_PORT", 8080).unwrapOr(8080)
env.set("EX_PORT", "abc").unwrap()
log env.int("EX_PORT", 8080).unwrapOr(8080)
```

## API reference

## API reference

Every member of `std:env`, generated from the source of OSL.

### Functions

#### `env.all() object`

all returns every environment variable as an object of strings.

#### `env.bool(string key, boolean fallback) result<boolean, error>`

bool reads an environment variable as a boolean, returning fallback when
it is unset or blank. 1, true, t, yes, y and on are true; 0, false, f,
no, n and off are false, in any case. Fails with ParseError for anything
else.

#### `env.cwd() string`

**Deprecated.** Deprecated: use process.cwd() instead.

cwd returns the working directory, or "".

#### `env.expand(string text) string`

expand replaces $NAME and ${NAME} in text with environment variables;
unset ones become "".

#### `env.file(string path) *env.File`

**Deprecated.** Deprecated: use env.from({}) and file.setPath(path), or env.parseFile(path) for a file that exists.

file returns an empty File handle for path.

#### `env.from(object values) *env.File`

from builds a File handle with no path from an object; values become
strings.

#### `env.get(string key, any ...fallback) string`

get returns an environment variable's value. When it is unset or empty
it returns fallback, or "" without one; use env.has to tell unset from
empty.

#### `env.getBool(string key, boolean def) boolean`

**Deprecated.** Deprecated: use env.bool(key, def) instead, which fails on malformed values.

getBool reads a boolean, or def when unset or malformed.

#### `env.getDefault(string key, string def) string`

**Deprecated.** Deprecated: use env.get(key, def) instead.

getDefault returns a variable, or def when it is unset or empty.

#### `env.getFloat(string key, number def) number`

**Deprecated.** Deprecated: use env.number(key, def) instead, which fails on malformed values.

getFloat reads a number, or def when unset or malformed.

#### `env.getInt(string key, int def) int`

**Deprecated.** Deprecated: use env.int(key, def) instead, which fails on malformed values.

getInt reads a whole number, or def when unset or malformed.

#### `env.has(string key) boolean`

has reports whether an environment variable is set, even to "".

#### `env.home() string`

**Deprecated.** Deprecated: use sys.homeDir() instead.

home returns the home directory, or "".

#### `env.int(string key, int fallback) result<int, error>`

int reads an environment variable as a whole number, returning fallback
when it is unset or blank. Fails with ParseError when it is set to
something else, such as PORT=abc.

#### `env.isDev() boolean`

isDev reports whether env.mode() is "dev" or "development".

#### `env.isProd() boolean`

isProd reports whether env.mode() is "prod" or "production".

#### `env.isTest() boolean`

isTest reports whether env.mode() is "test" or "testing".

#### `env.keys() string[]`

keys returns the names of every environment variable, sorted.

#### `env.load(any ...paths) result<unit, error>`

load reads .env files (".env" when none are given) and sets their
variables, keeping any that are already set. Use
env.parseFile(path).apply({override: true}) to replace them. Every file
that can be read is applied, in order, even when an earlier one is
missing; then load fails with the first error: NotFoundError for a
missing file, PermissionError or IOError for an unreadable one, and
ValueError for a key the OS rejects.

#### `env.local() boolean`

**Deprecated.** Deprecated: use env.load() instead.

local loads .env and reports whether it was read.

#### `env.localOverload() boolean`

**Deprecated.** Deprecated: use env.parseFile(".env").apply({override: true}) instead.

localOverload loads .env, replacing set variables, and reports whether
it was read.

#### `env.missing(any ...keys) string[]`

missing returns the listed environment variables that are unset or
empty, in the order given.

#### `env.mode() string`

mode returns the app's mode from APP_ENV, OSL_ENV or NODE_ENV (the first
that is set), lowercased, or "development".

#### `env.number(string key, number fallback) result<number, error>`

number reads an environment variable as a number, returning fallback
when it is unset or blank. Fails with ParseError when it isn't a number.

#### `env.overload(any ...paths) boolean`

**Deprecated.** Deprecated: use env.parseFile(path).apply({override: true}) instead.

overload loads .env files, replacing variables that are already set,
and reports whether every file was read.

#### `env.parse(string text) *env.File`

parse reads .env text into a File handle with no path.

#### `env.parseFile(string path) result<*env.File, error>`

parseFile reads a .env file into a File handle that remembers its path,
so file.save() writes back to it. Fails with NotFoundError when the file
doesn't exist, PermissionError or IOError.

#### `env.read(string path) *env.File`

**Deprecated.** Deprecated: use env.parseFile(path) instead, which fails when the file can't be read.

read reads a .env file into a File handle, which is empty when the file
can't be read.

#### `env.require(string key) result<string, error>`

require returns an environment variable that must be set. Fails with
NotFoundError when it is unset or empty.

#### `env.required(any ...keys) result<unit, error>`

required checks that every listed environment variable is set and not
empty. Fails with NotFoundError naming all the missing ones.

#### `env.set(string key, string value) result<unit, error>`

set sets an environment variable for this program and the processes it
starts. Spaces around key are ignored. Throws ValueError for an empty
name or one containing "=".

#### `env.stringify(object values) string`

stringify formats an object as .env text, one KEY=value line per key in
sorted order, quoting values that need it.

#### `env.unset(string key) result<unit, error>`

unset removes an environment variable. Removing one that isn't set does
nothing. Throws ValueError for an empty name or one containing "=".

#### `env.value(string key) env.Value`

**Deprecated.** Deprecated: use env.get, env.has, env.int, env.number or env.bool instead.

value returns a Value handle for a variable.

### `*env.Value`

#### `value.bool(boolean def) boolean`

**Deprecated.** Deprecated: use env.bool(key, def) instead.

bool returns the value as a boolean, or def.

#### `value.exists() boolean`

**Deprecated.** Deprecated: use env.has(key) instead.

exists reports whether the variable is set.

#### `value.fallback(string def) string`

**Deprecated.** Deprecated: use env.get(key, def) instead.

fallback returns the value, or def when it is unset or empty.

#### `value.float(number def) number`

**Deprecated.** Deprecated: use env.number(key, def) instead.

float returns the value as a number, or def.

#### `value.int(int def) int`

**Deprecated.** Deprecated: use env.int(key, def) instead.

int returns the value as a whole number, or def.

#### `value.key() string`

**Deprecated.** Deprecated: use env.get and env.has instead of value handles.

key returns the variable's name.

#### `value.string() string`

**Deprecated.** Deprecated: use env.get(key) instead.

string returns the raw value.

### `*env.File`

#### `file.all() object`

all returns the values as an object of strings.

#### `file.apply(object? options) result<unit, error>`

apply sets every value as an environment variable. Options: override
(boolean, default false) also replaces variables that are already set.
Fails with ValueError for a key the OS rejects, such as one containing
"=", after setting the others.

#### `file.applyOverload() boolean`

**Deprecated.** Deprecated: use file.apply({override: true}) instead.

applyOverload sets every value as an environment variable, replacing set
ones.

#### `file.bool(string key, boolean fallback) result<boolean, error>`

bool reads a value as a boolean, like env.bool.

#### `file.clear() *env.File`

clear removes every key. It mutates the file and returns it, so calls
can chain.

#### `file.expand(string key) string`

expand returns a value with $NAME and ${NAME} references replaced, first
from the file's own values (recursively, ignoring cycles) and then from
the environment; \$ stays a literal $.

#### `file.get(string key, any ...fallback) string`

get returns a value. When it is missing or empty it returns fallback,
or "" without one.

#### `file.getBool(string key, boolean def) boolean`

**Deprecated.** Deprecated: use file.bool(key, def) instead.

getBool reads a boolean, or def when missing or malformed.

#### `file.getDefault(string key, string def) string`

**Deprecated.** Deprecated: use file.get(key, def) instead.

getDefault returns a value, or def when it is missing or empty.

#### `file.getFloat(string key, number def) number`

**Deprecated.** Deprecated: use file.number(key, def) instead.

getFloat reads a number, or def when missing or malformed.

#### `file.getInt(string key, int def) int`

**Deprecated.** Deprecated: use file.int(key, def) instead.

getInt reads a whole number, or def when missing or malformed.

#### `file.has(string key) boolean`

has reports whether the file has key, even with an empty value.

#### `file.int(string key, int fallback) result<int, error>`

int reads a value as a whole number, like env.int.

#### `file.keys() string[]`

keys returns the names in the file, sorted.

#### `file.load() boolean`

**Deprecated.** Deprecated: use file.read() and then file.apply() instead.

load re-reads the file and sets its values as environment variables,
keeping set ones, and reports whether it was read.

#### `file.loaded() boolean`

**Deprecated.** Deprecated: env.parseFile and file.read() now fail when the file can't be read.

loaded reports whether the last read succeeded.

#### `file.merge(object values) *env.File`

merge sets every key of an object. It mutates the file and returns it,
so calls can chain. Blank keys are skipped.

#### `file.number(string key, number fallback) result<number, error>`

number reads a value as a number, like env.number.

#### `file.overload() boolean`

**Deprecated.** Deprecated: use file.read() and then file.apply({override: true}) instead.

overload re-reads the file and sets its values as environment variables,
replacing set ones, and reports whether it was read.

#### `file.path() string`

path returns the file's path, or "" when it has none.

#### `file.read() result<unit, error>`

read replaces the values with the contents of the file at file.path().
Fails with NotFoundError when it doesn't exist, PermissionError or
IOError, leaving the values unchanged. Throws StateError when the file
has no path.

#### `file.save() result<unit, error>`

save writes the values to file.path() as .env text (see env.stringify).
Fails with NotFoundError when the directory doesn't exist, PermissionError
or IOError. Throws StateError when the file has no path.

#### `file.set(string key, any value) *env.File`

set sets a value. It mutates the file and returns it, so calls can
chain. Non-string values are stored as their string form.
Throws ValueError for an empty key.

#### `file.setPath(string path) *env.File`

setPath sets the path file.read() and file.save() use. It mutates the
file and returns it, so calls can chain. An empty path means ".env".

#### `file.text() string`

text formats the values as .env text (see env.stringify).

#### `file.unset(string key) *env.File`

unset removes a key. It mutates the file and returns it, so calls can
chain.

#### `file.value(string key) env.Value`

**Deprecated.** Deprecated: use file.get, file.has, file.int, file.number or file.bool instead.

value returns a Value handle for key.
