SCUA

How-to

Read environment variables

Environment variables are authority, not inert data: they're invisible, often hold secrets (DATABASE_URL, AWS_SECRET_ACCESS_KEY), and span the whole process. So unlike command-line arguments (which are public and need no grant), reading the environment is a host-granted capability — the env module doesn't exist unless you turn it on.

#Granting the capability

scua --allow-env=PORT,HOST,LOG_LEVEL  script.scua   # grant exactly those three names (the safe default)
scua --allow-env                       script.scua   # grant every variable (trusted-host shorthand)

Without a grant, import env is a compile error, before a line runs:

$ scua script.scua
script.scua:1: module `env` needs the `env` capability, which this run was not granted — grant it with `--allow-env` on the CLI, or the host's capability API when embedding

The per-name allowlist is the point: a variable you didn't grant reads as nil even when it is set in the environment, so a script can never see a secret it wasn't given. Prefer --allow-env=NAME,...; reach for bare --allow-env only when the script and the operator are the same trusted party.

#Reading values

import env

let port = env.get("PORT") ?? "8080"      -- missing/un-allowlisted → nil; ?? gives a default
let host = env.get("HOST") ?? "localhost"

-- required variable
if env.get("API_KEY") == nil then
  error("API_KEY is required — set it in your environment")
end

env.get(name) returns the value as a string, or nil if the variable was unset or not in the allowlist. A missing variable is a normal nil, not an error. The module is read-only — there is no env.set.

#Loading a .env file

The usual development setup is a .env file beside the project. --env-file loads one and grants env for exactly the names in it:

# .env
DB_HOST=localhost
DB_PORT=5432
TOKEN='s3cr3t'
$ scua --env-file=.env app.scua

Naming the file is the grant — you don't also need --allow-env, and you shouldn't have to write the file's key list twice. Note what that means: with --env-file alone your script sees the file's names and nothing else. The real environment stays hidden, so a stray AWS_SECRET_ACCESS_KEY in your shell can't reach the script. Add --allow-env=NAME,... if you want specific real variables too.

A real environment variable wins over the file. That is the rule every dotenv library follows, and it is what makes .env a defaults file:

$ DB_HOST=prod.internal scua --env-file=.env app.scua    # DB_HOST is prod.internal, DB_PORT is 5432

--env-file is repeatable and applied in order, and nothing overwrites what is already set — so an earlier file wins over a later one:

$ scua --env-file=.env.local --env-file=.env app.scua     # .env.local overrides, .env fills the gaps

#The file format, and two things that catch people

KEY=value, # comments, an optional export prefix, and both quotings — 'literal' verbatim, "basic" processing \n, \r, \t, \\ and \". Either quote style may span lines. KEY= is the empty string, which is set, not absent. A repeated key keeps its last value, as other dotenv readers do — while across several --env-files the first file wins, because that is a different question: which line of a file owns a name, versus which file does.

  • A Windows path needs single quotes. "C:\temp" contains \t, so it reads as a tab. 'C:\temp' is verbatim and correct. (This is the same in Python's dotenv; it is a trap worth knowing once.)
  • ${VAR} is not interpolated. It stays literal text. This is deliberate: a value that could name another variable would be a way around the allowlist you just set. Write the composed value out.

Anything the parser can't read — a line with no =, an unterminated quote, junk after a closing quote — stops the run and says so, rather than guessing.

#Reading a .env file as data

If you want the contents of a .env file rather than the authority — a deployment tool inspecting someone else's file, say — dotenv.parse is a pure parser, no grant needed:

import dotenv

match dotenv.parse("PORT=8080\nTOKEN='s3cr3t'\n")
  Ok(t) -> print(`{t.PORT} {t.TOKEN}`)
  Error(why) -> print(`bad .env: {why}`)
end
$ scua parse_env.scua
8080 s3cr3t

It reads the same grammar as --env-file, so the two can't disagree about what a file means. Reading one off disk needs the fs capability, as usual.

#What you can rely on

  • Snapshot at launch. The environment is captured once when the script starts, so env.get is a pure lookup with no surprises mid-run — and a run's inputs are reproducible (recorded alongside the RNG seed). A change to the environment after launch is not seen.
  • Per-partition. Inside a spawned actor, env is not granted — authority doesn't leak across partitions. Pass an actor what it needs in its starting message.
  • UTF-8. Values are UTF-8 strings. A non-UTF-8 allowlisted value fails the launch with a clear message (rather than silently returning nil); under bare --allow-env, a non-UTF-8 variable is skipped with a warning.

See the runnable examples/env.scua (scua --allow-env=PORT,HOME examples/env.scua).