Skip to content

Options And Discovery

Chris Michael edited this page Oct 7, 2026 · 1 revision

Pudu

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.

Which files are read

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")

Overwriting

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.

Related

Clone this wiki locally