SCUA

How-to

Read config files (TOML and INI)

SCUA reads the common config formats out of the box. toml and ini are pure modules — parsing text needs no capability grant — and both parse to a nested table, which is the whole point: the result pairs directly with first-class paths, so you read config with cfg @ "server/port" instead of a chain of field lookups.

(JSON is also built in — see json in the builtins reference. For YAML, see "What's not here yet" below.)

#TOML

import toml

let text = "
[server]
host = \"0.0.0.0\"
port = 8080
debug = true
"

match toml.parse(text)
  Ok(cfg) -> do
    print(cfg @ "server/host")    -- "0.0.0.0"
    print(cfg @ "server/port")    -- 8080  (a real int)
    print(cfg @ "server/debug")   -- true  (a real bool)
    print(cfg @ "server/region" ?? "us-east")  -- missing key → default
  end
  Error(why) -> print(`bad config: {why}`)
end

toml.parse(s) returns Ok(table) or Error(reason) — a malformed document is a value you handle, not a crash. Sections nest (a dotted [a.b] header nests two levels), and values keep their type.

This is TOML 1.0, so the rest of the format is there too:

import toml

let text = "
ports = [8080, 8081]          # an array
ratio = 0.75                  # a float
origin = { x = 0, y = 0 }     # an inline table
released = 2026-08-19T09:00:00Z

[[worker]]                    # an array of tables
name = \"alpha\"

[[worker]]
name = \"beta\"
"

match toml.parse(text)
  Ok(cfg) -> do
    print(cfg @ "ports")           -- [8080, 8081] — a real array
    print((cfg @ "ports")[0])      -- 8080
    print(cfg @ "ratio")           -- 0.75
    print(cfg @ "origin/y")        -- 0
    print(cfg @ "released")        -- 2026-08-19T09:00:00.000Z — a datetime, not a string
    print(len(cfg @ "worker"))     -- 2
    print((cfg @ "worker")[1] @ "name")  -- "beta"
  end
  Error(why) -> print(`bad config: {why}`)
end

Arrays become arrays, [[sections]] an array of tables, and dates a datetime you can do arithmetic on. Integers accept the 0x / 0o / 0b bases and 1_000 underscores; floats accept inf and nan; strings come in all four TOML flavours ("basic", 'literal', """multi-line""", '''multi-line literal''') with escapes processed where TOML says they should be.

One gap, and it is deliberate. A bare local time (07:32:00 — a time of day with no date) reads as a string, because SCUA has no time-of-day type and inventing a date for it would be a silent lie. Everything else in TOML 1.0 parses to its own type.

A date that does not exist is an error, not a nearby date: 1979-99-99 and 2026-02-29 are refused rather than rolled forward into a real day. And a bad document tells you wheremalformed TOML at line 5 — rather than leaving you to bisect the file.

#INI

import ini

let text = "
[db]
host = db.internal
port = 5432
"

match ini.parse(text)
  Ok(cfg) -> do
    print(cfg @ "db/host")               -- "db.internal"
    print(tonumber(cfg @ "db/port"))     -- 5432
  end
  Error(why) -> print(`bad config: {why}`)
end

INI has no value types, so every value is text — use tonumber where you want a number. [section] headers nest (dotted names like [db.pool] nest further), # and ; start full-line comments, and both key = value and key: value are accepted.

#Writing config back out

toml.encode(table) is the other direction — a table in, TOML text out:

import toml

let cfg = {
  name = "my-service",
  ports = [8080, 8081],
  server = { host = "0.0.0.0", tls = true },
}

print(toml.encode(cfg))
$ scua write_config.scua
name = "my-service"
ports = [8080, 8081]

[server]
host = "0.0.0.0"
tls = true

Keys come out sorted, so the output is the same every run — a config file that reshuffles between runs is useless in version control. Sub-tables become [section] headers and arrays of tables become [[section]], which is what you would have written by hand.

A date's value survives a read-modify-write; its spelling does not. toml.parse turns every TOML date into a datetime, which is an instant plus a UTC offset and holds no record of having been written date-only — so released = 2026-08-19 read in and written back out comes back as 2026-08-19T00:00:00Z. Same instant, longer spelling. If you are rewriting a file whose exact formatting matters, edit the text rather than round-tripping it through a table.

Two more things to know. Exact decimal and money values are written as strings, because TOML's only number types are 64-bit int and float and either would drop the exactness those types exist to keep (json.encode does the same). And a value with no TOML form at all — bytes, a vector, a function — is a fault, not a silent approximation; convert it first.

#Reading from a file

The examples above use inline strings so they're self-contained. In a real tool you'd read the file with the fs capability and feed it straight in — the ? operator threads the read failure through:

import fs
import toml

fn load_config()
  let text = fs.read_text("config.toml")?   -- Error short-circuits if the file is missing/!UTF-8
  return toml.parse(text)                    -- Ok(table) | Error(reason)
end

Run it with scua --allow-fs=. tool.scua.

#Why paths

Because the parsers return nested tables, the same @ path access, set, and path"..." values that work on any table work on parsed config — and a missing key reads as nil, so ?? gives you a default in one line. See the runnable examples/config.scua.

#What's not here yet

  • YAML — deliberately deferred. Full YAML is large and ambiguous (anchors, tags, the "Norway problem"); a safe, strictly-bounded subset is planned but its exact boundary needs pinning first. Until then, prefer TOML or JSON for new config.
  • Writing INI (ini.encode) — INI is parse-only. TOML has toml.encode, above.
  • TOML local times07:32:00 reads as a string; see the note above.