The framework loads .env files before running any command, following the
precedence rules of
Symfony Dotenv.
Values are parsed with a hand-written expander, not eval, so a .env file
can set variables but never run commands.
Files are looked up in the project root: the directory containing
cli.sh, or its parent when the running script is dist/cli.sh. Symlinks
are resolved first. The current working directory is never consulted, so
running the CLI from inside another project does not pick up that project's
.env.
| Order | File | Loaded when |
|---|---|---|
| 1 | .env |
always; .env.dist is used instead if .env is absent |
| 2 | .env.local |
unless the environment is test |
| 3 | .env.<env> |
unless the environment is local |
| 4 | .env.<env>.local |
unless the environment is local |
The environment name comes from APP_ENV, then CLI_ENV, then defaults to
dev. It is re-read after .env.local so that file can switch it.
Commit .env (or .env.dist) as the template with safe defaults. Keep real
values in .env.local and .env.<env>.local, which the default .gitignore
excludes.
- A variable that already exists in the process environment is never
overwritten.
APP_ENV=prod ./cli.sh ...beats every file. - A variable set by an earlier
.envfile is overwritten by a later one, which is what makes.env.localan override layer. overload_env(ordebug:reload:env --override=true) flips the first rule and lets files replace existing variables. Use it deliberately.
Every variable the loader sets is exported, and its name is appended to
DOTENV_VARS, a comma-separated list the debug commands use.
# Comments start with a hash
APP_NAME=myapp
export DB_HOST=localhost # "export " prefix is accepted and ignored
DB_URL=postgres://${DB_HOST}/app # ${VAR} and $VAR expand
TIMEOUT=${TIMEOUT:-30} # default when unset or empty
CACHE_DIR=${CACHE_DIR:=/tmp/c} # like :- but also exports CACHE_DIR
GREETING="Hello\n${APP_NAME}" # double quotes: expansion plus \n \r \t \" \\
LITERAL='${NOT_EXPANDED}' # single quotes: taken verbatim
PATH_WITH_HASH=/srv/app#1 # inline comments need a space before the hashRules:
- Names must match
[a-zA-Z_][a-zA-Z0-9_]*. Other lines are skipped silently, as are lines without=. - Whitespace around the name and around an unquoted value is trimmed.
$(...)and backticks are not command substitution; they stay as literal text.tests/test_env_security.shchecks this.- One variable per line. Multiline values and line continuations are not supported.
- A file starting with a UTF-8 byte-order mark is rejected with an error.
Loaded variables are ordinary environment variables:
cmd_db_ping() {
: "${DB_URL:?Set DB_URL in .env.local}"
info "Pinging $DB_URL"
}The ${VAR:?message} form is the cheapest way to make a variable mandatory.
| Command | Shows |
|---|---|
debug:dotenv |
Environment name, project root, which of the five files exist, the load order, and a table of every variable the loader set |
debug:env <NAME> |
The value and length of one variable, and whether it came from a .env file |
debug:reload:env [--override=true] |
Re-runs the loader, optionally in override mode |
debug:dotenv and debug:env print values in clear text. Do not run them
where the output is logged.
Available to plugins and tests:
| Function | Purpose |
|---|---|
load_env [dir] [env] [override] |
Load the file set from dir (default: project root) |
overload_env [dir] [env] |
load_env with override on |
load_env_from_file <file> [override] |
Load a single file |
parse_env_value <raw> |
Apply quoting and expansion rules to one value |
expand_env_variables <text> |
Expand $VAR, ${VAR}, ${VAR:-d}, ${VAR:=d} in a string |
get_project_root |
The directory the .env files are read from |