Repository navigation
Options And Discovery
Options.Options is a record. Build it from Options.defaults() with a record update, or change it through the Configuring chain — both give the same value:
let byRecord = Options.Options{..Options.defaults(), strict: true, expansion: true}
let byChain = Options.defaults().withStrict().withExpansion()
| Field | Default | Chain |
|---|---|---|
strict |
false |
withStrict, withoutStrict
|
files |
[".env"] |
withFiles(&paths); an empty list restores [".env"]
|
sources |
[] |
withSources(&sources) |
encoding |
Encoding.Utf8 |
withEncoding(encoding) |
trimValues |
false |
withTrimValues, withoutTrimValues
|
overwrite |
true |
withOverwrite, withoutOverwrite
|
probe |
None |
withProbe() (four levels), withProbeLevels(n), withoutProbe
|
exportSyntax |
false |
withExportSyntax, withoutExportSyntax
|
inlineComments |
true |
withInlineComments, withoutInlineComments
|
expansion |
false |
withExpansion, withoutExpansion
|
cascade |
false |
withCascade, withEnvironment(name), withoutCascade
|
environmentName |
None |
withEnvironment(name) |
directory |
"." |
inDirectory(path) |
Options.validate lists every conflict at once, and every load starts by calling it: given files with probing or the cascade, sources with files, probing, or the cascade, a negative probe depth, and an empty file list. A load with any of them answers InvalidOptions naming all of them.
| Mode | How | Files |
|---|---|---|
| Given | files |
each path in order, relative paths located in directory
|
| Probing | withProbe() |
the first .env found in directory or up to n parents |
| Cascade | withCascade() |
whichever of .env, .env.local, .env.<name>, .env.<name>.local exist, in that order |
The cascade's name is environmentName, else PUDU_ENV, else PROFILE — the variable the standard library's configuration reads its profile from — whichever is first set and not blank. A name holding .., a path separator, or a character some platform refuses in a file name is refused with UnsafeEnvironmentName, even in a lenient load, so a variable cannot point the cascade outside its directory. Cascade and probing combine: the layered files are looked for upward.
module Ex04Cascade
import Std.Fs as Fs
import Std.Io as Io
import Std.Map as Map
import Std.Path as Path
import PuduLangEnvironment as Environment
import PuduLangEnvironment.Loader as Loader
import PuduLangEnvironment.Options as Options
import PuduLangEnvironment.Variables as Variables
fn layers(project: Str) -> Result[Int, Fs.FsError] {
let nested = Path.join(project, "services/api")
let _services = Io.makeDirectory(Path.join(project, "services"))
let _api = Io.makeDirectory(nested)
let _shared = Io.write(Path.join(project, ".env"), "HOST=localhost\nPORT=8080\n")
let _local = Io.write(Path.join(project, ".env.local"), "PORT=8081\n")
let _staging = Io.write(Path.join(project, ".env.staging"), "HOST=staging.internal\n")
let process = Map.fromPairs(&[("PUDU_ENV", "staging")])
let options = Options.defaults().inDirectory(nested).withCascade().withProbe()
match Loader.loadOver(&options, &process) {
case Ok(variables) => {
for source in variables.sources { let _read = Io.writeLine("read " + Path.nameOf(source)) }
let _host = Io.writeLine("HOST = " + show(Variables.find(&variables, "HOST")))
let _port = Io.writeLine("PORT = " + show(Variables.find(&variables, "PORT")))
Ok(0)
}
case Err(problem) => {
let _said = Io.writeErrorLine(Environment.describe(&problem))
Ok(1)
}
}
}
export fn main() -> Int {
match Fs.withTemporaryDirectory("cascade-", layers) {
case Ok(status) => status
case Err(_) => 1
}
}
Output:
read .env
read .env.local
read .env.staging
HOST = Some("staging.internal")
PORT = Some("8081")
With overwrite on (the default), a later layer replaces an earlier one, and a loaded value replaces a process variable. With withoutOverwrite(), the first assignment of each key wins, and a process variable that is set and not empty is kept. In the program above the cascade read PORT from .env and then .env.local; the later value won.