File structure
The header, comments, keys and blocks — and what happens when the same key is written more than once.
The version header
Every file — an imported one too — begins with #@version and the format version it is written against. The = is optional; the version is SemVer. The smallest valid file is the header alone:
#@version = 1.0
%{}Whitespace and comments
Statements are separated by whitespace; one per line is the convention. Inside a block, commas may separate statements as well. A # followed by a space starts a comment that runs to the end of the line. There are no block comments.
#@version = 1.0
# A comment runs to the end of the line.
name = "api" # so does a trailing one
limits { soft = 10, hard = 20 }%{"name" => "api", "limits" => %{"soft" => 10, "hard" => 20}}Keys
A bare key starts with a letter and continues with letters, digits, _, + and -; it may end in ? or !. So logger, handle-otp-reports, enabled? and ready! are all keys. The same rule names variables and atoms.
Reserved everywhere: nil, true, false, inf, +inf, -inf. The words for, in, from, as and import only mean something where a loop or an import expects them, and are ordinary keys elsewhere.
Quoted keys
Anything else — spaces, dots, a reserved word — goes in quotes. Double quotes allow escapes, single quotes are literal. A quoted key can be one segment of a dotted path.
headers { "Content-Type" = "application/json" }
foo."bar baz".dronf = "fnord"
"true" = 1%{"headers" => %{"Content-Type" => "application/json"},
"foo" => %{"bar baz" => %{"dronf" => "fnord"}},
"true" => 1}Secret keys
A * in front of any key segment marks the value as secret. The value itself is unchanged; an implementation wraps it so that it is redacted when printed or logged. Secrecy follows the value: a string that interpolates a secret is itself secret, and prints with that part redacted.
*password = "hunter2"
db.*token = "t0k3n"
url = "postgres://app:%{password}@db.internal"%{"password" => [~~REDACTED~~],
"db" => %{"token" => [~~REDACTED~~]},
"url" => "postgres://app:[~~REDACTED~~]@db.internal"}Assignment
A statement assigns a value to a key: key = value. The specification lets you drop the = (port 8080), but the reference implementation misreads a single bare key followed by a string as an import, so writing = is the portable choice. A colon is never an assignment.
Paths and blocks
A block { … } groups statements under a key; a dotted path reaches into nested blocks directly. Both build the same tree, and they mix freely. These four lines produce the identical result:
foo { bar { baz = "dronf" } }
foo { bar.baz = "dronf" }
foo = { bar = { baz = "dronf" } }
foo.bar.baz = "dronf"%{"foo" => %{"bar" => %{"baz" => "dronf"}}}Blocks are CASC’s only map type, and a block stands only on the right-hand side of a statement. A list can hold lists and tuples, but not blocks.
Disabled statements
A # written directly against a statement switches it off, however many lines it spans. The key is absent from the result — not nil. It is the quick way to try a configuration without a setting and keep it at hand.
section {
#ignored = 1
#sub { remove = yes }
kept = true
}%{"section" => %{"kept" => true}}Merging and overrides
A key may be written any number of times, in one file or across imports. Statements apply in order:
- Blocks deep-merge, at every depth.
- Scalars, lists and tuples are replaced: the last write wins.
- A change of type replaces too: a block after a scalar is a block.
When the default is not what you want, a sigil in front of the key says so:
| Sigil | Effect |
|---|---|
~key { … } | Replace the block instead of merging into it |
+key = [ … ] | Append to a list (a plain assignment if the key is new) |
-key = [ … ] | Remove these elements from a list, by equality |
-key | Delete the key; deleting a missing key does nothing |
Tuples are never merged: + or - against a tuple is an error. When several prefixes combine, the order is #, then the sigil, then *, as in #~*database { … }.
# base.casc
server { host = "0.0.0.0", port = 8080 }
tags = ["a", "b", "c"]
feature.legacy_mode = true
# override.casc
~server { port = 9090 }
+tags = ["d"]
-tags = ["b"]
-feature.legacy_mode%{"server" => %{"port" => 9090},
"tags" => ["a", "c", "d"],
"feature" => %{}}