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
where — malformed 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 hastoml.encode, above. - TOML local times —
07:32:00reads as a string; see the note above.