Introduction
MICAL is a simple, line-oriented configuration language. It is designed to be flat, explicit, and easy to read.
Unlike complex hierarchical formats, MICAL treats configuration as a linear sequence of key-value pairs, grouped visually but remaining logically flat.
This documentation guides you through installing the MICAL tools and understanding the language specification.
- Installation: How to get started with the MICAL CLI.
- Language Overview: A quick tour of MICAL’s syntax and concepts.
- Specification: Detailed reference documentation.
Installation
MICAL can be installed using Cargo (the Rust package manager) or Nix.
Using Cargo
MICAL is available on crates.io. You can install the CLI tool directly using:
cargo install mical-cli
Using Nix
If you use Nix, you can run or install MICAL from the GitHub repository.
Running without installing
You can run MICAL immediately without adding it to your profile:
nix run github:mical-lang/mical
Installing to your profile
To install MICAL permanently into your Nix profile:
nix profile install github:mical-lang/mical
Building from Source
If you prefer to build from the source code:
git clone https://github.com/mical-lang/mical.git
cd mical
cargo install --path .
Language Overview
MICAL is a line-oriented configuration language designed for flat structures and readability. A MICAL file is a sequence of key-value entries. Scope nesting is expressed by indentation, but the output remains logically flat.
Key-Value Entries
Each line represents a single entry: a key followed by a space and a value.
host localhost
port 8080
enabled true
{
"host": "localhost",
"port": 8080,
"enabled": true
}
Keys
Keys come in two forms:
- Word key: any sequence of non-whitespace characters.
name,server.port,-flag,42are all valid keys. - Quoted key: a string enclosed in
"or', allowing spaces and empty keys.
name hello
server.port 8080
"user name" Alice
"" empty-key
Values
The type of a value is determined by its content on the line.
true/false→ Boolean (only when the entire value is the keyword alone)- Integer literal (e.g.
42,+1,-10) → Integer (only when the entire value is the literal alone) "..."/'...'→ Quoted String (must be the sole value; trailing content is an error)|/>followed by newline → Block String- Everything else → Line String (the rest of the line, as-is)
The fallback to Line String is intentional: anything that does not exactly match the typed forms is treated as a plain string.
flag true
count 42
name "Alice"
path /usr/local/bin
text 10 items
note true story
{
"flag": true,
"count": 42,
"name": "Alice",
"path": "/usr/local/bin",
"text": "10 items",
"note": "true story"
}
Note that 10 items is a Line String (not integer) and true story is a Line String (not boolean) because they contain trailing content.
Comments
A # at the start of a line, or preceded by a space, starts a comment that runs to the end of the line. A # without a preceding space is part of the value.
# This is a comment
key value # this is an inline comment
url https://example.com/page#section
{
"key": "value",
"url": "https://example.com/page#section"
}
Prefix Blocks
Blocks group entries under a common key prefix. A key alone on its line, followed by more-indented entries, opens a block. Blocks do not create nested objects and do not insert any separator (such as .).
server.
host localhost
port 8080
{
"server.host": "localhost",
"server.port": 8080
}
Since no separator is inserted, omitting the . changes the result:
http_
port 80
This produces the key http_port, equivalent to writing http_port 80.
Block Strings
Multi-line string values use the | (literal) or > (folded) header. Indentation of the line immediately after the header defines the base indent, which is stripped from each line.
description |
MICAL is simple.
It keeps your config clean.
{
"description": "MICAL is simple.\nIt keeps your config clean.\n"
}
An explicit indentation indicator (|2) fixes the base indent when the first line should itself start with spaces. Chomping indicators (+ keep, - strip, default clip) control trailing newlines. See Block Strings for the full algorithm.
Specification
This section provides the detailed specification for the MICAL language.
- Syntax & Structure: File encoding, line processing, comments, directives, indentation, and the overall structure of a MICAL file.
- Keys: Word keys and quoted keys, their syntax rules, and error cases.
- Values: Type determination algorithm, each value type, and the fallback behavior.
- Block Strings: Multi-line string syntax, base indent detection, the explicit indentation indicator, line classification, styles, and chomping indicators.
- Prefix Blocks: Indentation-based block syntax, opener recognition, body indentation rules, prefix concatenation, and nesting.
Syntax & Structure
MICAL source files are UTF-8 encoded text files. The language is line-oriented: line breaks serve as the primary delimiters between entries.
Line Endings
Both LF (\n) and CRLF (\r\n) are recognized. The lexer normalizes CRLF to a single newline token.
Whitespace
MICAL distinguishes three whitespace characters:
- Space (
U+0020): used for indentation and as a separator between keys and values. - Tab (
U+0009): forbidden for indentation. A line that begins with a tab (after any leading spaces) produces a parse error. This rule does not apply inside the body of a block string, where lines are classified by their leading spaces alone and tabs beyond the base indent are literal content. - Newline (
U+000A): terminates lines and entries.
All other characters are non-whitespace and form part of keys or values.
Throughout this specification, a blank line (also called a whitespace-only line) is a line that is empty or consists solely of spaces. A line whose leading whitespace includes a tab is not blank — the tab rule above applies to it.
Indentation
Indentation must consist of spaces only. Indentation is semantic: it defines the structure of Prefix Blocks and the content boundaries of Block Strings.
Top-level items must start at column 0 (no indentation). A line indented deeper than the current context, where no prefix block or block string introduces a new level, is a parse error (see Prefix Blocks).
Blank lines and comment-only lines are exempt from indentation rules: they may appear at any indentation and never open or close a block.
Structure of a Source File
A MICAL file consists of a sequence of items. Each item is one of:
- Entry — a key-value pair on a single line.
- Prefix Block — a key alone on its line, followed by a body of more-indented items. See Prefix Blocks.
- Comment — see Comments.
- Directive — begins at column 0 with
#immediately followed by a word (no space).
Blank lines and comment-only lines are ignored between items.
Comments
A comment begins with a # that is either:
- at the start of a line (after optional indentation), or
- preceded by at least one space,
and extends to the end of the line. The comment, together with any spaces immediately preceding it, is removed before the line is interpreted.
# This is a full-line comment
key value # this is an inline comment
{ "key": "value" }
A # that is not preceded by whitespace is a literal character:
path /tmp/file#1
key val#ue
{
"path": "/tmp/file#1",
"key": "val#ue"
}
Comments are not recognized in the following contexts, where # is always literal:
- inside quoted strings and quoted keys,
- inside the body of a block string.
Because comment stripping happens before the value is interpreted, it affects type determination: a 42 # count produces the integer 42, and desc | # note opens a block string.
A value that needs to contain # (space, hash, space) must be written as a quoted string or a block string:
msg "before # after"
{ "msg": "before # after" }
Directives
A # at column 0 (before any indentation) immediately followed by a word (no space between # and the word) is parsed as a directive.
#include path/to/file
#schema https://example.com/schema.json
A directive consists of a name (the word after #) and arguments (the rest of the line, parsed as a Line String; inline comments are recognized in the argument part). It is syntactically distinct from a comment because there is no space between # and the first character.
Directives are a kind of comment: they never contribute key-value entries to the output. Unlike plain comments, they are addressed to the processor and may influence how it evaluates the file — for example, an implementation may support file inclusion via #include. An implementation is free to ignore any directive it does not support; no directive is part of the core language semantics.
Directives are recognized only at column 0. A # that appears after indentation, or anywhere else on a line (preceded by a space), is always a comment, even if it is immediately followed by a word:
#indented_word value
key value #word
The first line is a comment. The second line is an entry whose value is value — #word is an inline comment.
Keys
A key identifies a configuration entry. In an entry, the key is followed by whitespace and then a value. A key alone on its line (with no value) opens a prefix block when it is followed by a more-indented item.
There are two kinds of keys: word keys and quoted keys.
The separator between a key and a value must be spaces. Tab characters in the separator position are not allowed and produce a parse error (see Key-Value Separator).
Word Key
A word key is a contiguous sequence of non-whitespace characters. It is terminated by the first space, tab, newline, or EOF.
A word key may start with any character that is not whitespace, a quote (", '), or # (a line starting with # is a comment or directive): letters, digits, and punctuation such as ., -, + are all allowed. Notably, tokens like true, false, and numeric literals (42, -5) are valid word keys when they appear in key position.
hello world
42 value
-57 value
+13 value
true value
false value
server.port 8080
In all cases above, the first token on the line is the key and the text after the separating space is the value. Because 42, -57, true, etc. are followed by content, they are parsed as word keys, not as typed values.
A word key consumes all characters until whitespace or EOF, regardless of what those characters are. After the first character, even quotes and # are part of the key:
a#b value
can't value
These produce the keys a#b and can't.
Key without a value
A key alone on a line is interpreted by looking at the following lines:
- If the next item line (skipping blank lines and comment-only lines) is indented deeper than the key, the key opens a prefix block.
- Otherwise, it is a parse error: “missing value for the key”.
lonely
next 1
lonely is followed by next 1 at the same indentation, so it is not a block opener — this produces the error “missing value for the key”.
Note that because inline comments are stripped before interpretation, a line like key # comment is treated the same as a key alone on its line.
Quoted Key
A quoted key is enclosed in matching double quotes ("...") or single quotes ('...'). Quoted keys may contain any character that is valid in a string literal, including spaces and #. Empty quoted keys are allowed.
"double" value
'single' value
"key with spaces" value
"" value
'' value
Escape sequences inside quoted keys follow the same rules as quoted strings: \\ and the matching quote character can be escaped with a backslash.
A quoted key alone on its line can open a prefix block, following the same rule as word keys.
Quoted key errors
After the closing quote, the next character must be whitespace (space, tab, newline) or EOF. If non-whitespace characters appear immediately after the closing quote, a parse error is produced:
"quoted"ppp value
This produces the error: “unexpected token after quoted key”. A # immediately after the closing quote (without a separating space) is also this error, not a comment.
An unclosed quoted key (where the closing quote is missing before the end of the line) produces the error: “missing closing quote”.
"unterminated value
Key-Value Separator
The key and the value are separated by one or more spaces. Multiple spaces between the key and the value are allowed and are purely cosmetic:
a value
b 42
c true
Tab characters between the key and the value produce the error: “tab separating is not allowed”.
Duplicate Keys
Multiple entries may share the same key. Each occurrence is a distinct entry and all are preserved in the evaluated output.
tag web
tag server
tag production
{
"tag": ["web", "server", "production"]
}
This produces three entries, all with the key tag. No entry is overwritten or merged.
The same rule applies to keys formed by prefix concatenation:
item.
tag important
item.
tag urgent
{
"item.tag": ["important", "urgent"]
}
Both entries with key item.tag are preserved.
Values
A value is the right-hand side of a key-value entry. MICAL determines the type of a value by inspecting the tokens that follow the separator space on the same line.
Type Determination Algorithm
After parsing the key and consuming the separator space(s), the parser first strips any inline comment and trailing spaces from the rest of the line, then examines the remaining tokens to decide which value type to produce. The algorithm is:
- If the first token is a double or single quote (
"or'), parse a Quoted String. - If the first token is
|or>:- If, after an optional indentation indicator (a digit
1–9) and an optional chomping indicator (+or-), the rest of the line is blank, parse a Block String. - Otherwise, fall through to Line String.
- If, after an optional indentation indicator (a digit
- If the first token is
trueorfalseand the rest of the line is blank, parse a Boolean. - If the first token is a numeral and the rest of the line is blank, parse an Integer.
- If the first token is
+or-, the second token is a numeral, and the rest of the line is blank, parse an Integer (with sign). - Otherwise, parse a Line String (fallback).
“The rest of the line is blank” means that, after comment stripping, the next token is a newline, EOF, or trailing space(s) followed by newline/EOF.
Because comments are stripped first, an inline comment does not demote a typed value to a Line String:
a 42 # count
b true # enabled
{
"a": 42,
"b": true
}
Quoted String
A quoted string begins and ends with matching quote characters ("..." or '...').
a "hello"
b 'world'
c ""
d ''
{
"a": "hello",
"b": "world",
"c": "",
"d": ""
}
Within a quoted string, the backslash \ serves as an escape character. The following escape sequences are recognized:
| Sequence | Result |
|---|---|
\\ | Literal backslash |
\" | Double quote |
\' | Single quote |
\n | Newline (LF) |
\r | Carriage return (CR) |
\t | Tab |
All six sequences are recognized regardless of the quoting style (single or double). Any other character following a backslash is an error.
A # inside a quoted string is literal; comments are not recognized within quotes:
msg "before # after"
{ "msg": "before # after" }
Newlines cannot appear inside a quoted string; reaching a newline before the closing quote produces the error: “missing closing quote”.
A quoted string must be the entire value on the line (an inline comment after it is allowed). If any non-whitespace, non-comment content appears after the closing quote, the error “unexpected token after value” is produced:
a "value" extra
b "value" # comment
The first line is a parse error. The second line is a valid quoted string "value".
Boolean
The tokens true and false are parsed as boolean values only when they constitute the entire value on the line (with at most trailing whitespace and an inline comment).
a true
b false
{
"a": true,
"b": false
}
If any other content follows on the same line, the value falls back to a Line String:
a trueish
b falsehood
c true value
d false value
{
"a": "trueish",
"b": "falsehood",
"c": "true value",
"d": "false value"
}
Integer
An integer is an optional sign (+ or -) followed by a numeral. Supported numeral formats:
- Decimal:
0,42,1_000 - Binary:
0b1010 - Octal:
0o777 - Hexadecimal:
0xFF,0xDEAD_BEEF
Underscores may be used as visual separators within digits. An integer is recognized only when it constitutes the entire value on the line (with at most trailing whitespace and an inline comment).
a 0
b 42
c +1
d -1
e 0xFF
{
"a": 0,
"b": 42,
"c": 1,
"d": -1,
"e": 255
}
If any other content follows on the same line, the value falls back to a Line String:
a 42 items
b -10 trailing
c + 1
d +
{
"a": "42 items",
"b": "-10 trailing",
"c": "+ 1",
"d": "+"
}
Note that + 1 (with a space between the sign and the numeral) is a Line String, not an integer. The sign must be immediately adjacent to the numeral.
Line String
The Line String is the fallback value type. When the value does not match any of the above types, the parser consumes all remaining characters on the line (after comment stripping, up to the newline or EOF) as a single string token.
key value
name hello world
path /usr/local/bin
{
"key": "value",
"name": "hello world",
"path": "/usr/local/bin"
}
Line Strings preserve quotes and other punctuation literally — there is no array or object syntax in value position:
a value "quoted" text
b [1, 2, 3]
{
"a": "value \"quoted\" text",
"b": "[1, 2, 3]"
}
Note that quotes appearing mid-line do not protect a # from comment recognition; comment stripping is positional (see Comments):
a value "x # y" tail
{ "a": "value \"x" }
A # not preceded by whitespace is part of the Line String; a # preceded by a space starts a comment:
a hello#world
b hello # comment
{
"a": "hello#world",
"b": "hello"
}
A Line String therefore cannot contain # — use a Quoted String or a Block String for such values.
Trailing spaces
For all value types, trailing spaces before the newline (or before an inline comment) are stripped and not included in the value. This applies uniformly to Booleans, Integers, and Line Strings.
a hello·
b true·
c 42·
(where · represents a trailing space)
All three entries have the trailing space stripped: "hello", true, 42.
Block String
Block Strings are multi-line values. Their syntax and algorithm are described in detail in Block Strings.
Block Strings
Block strings are multi-line string values. They provide control over indentation stripping, newline handling, and text folding.
Header Syntax
A block string begins with a header on the same line as the key:
key <style>[indent][chomp]
content line 1
content line 2
The header consists of, in this fixed order:
- Style indicator (required):
|for literal style,>for folded style. - Indentation indicator (optional): a single digit
1–9. See Explicit Indentation. - Chomping indicator (optional):
+(keep),-(strip), or omitted (clip).
After the header, only whitespace and an optional inline comment may appear before the newline (or EOF). If any other content follows on the same line, the value is not a block string and falls back to a Line String:
a |not block
b >not fold
c |+not block
d |abc
e |-2
f | # comment
Values a through e are Line Strings: "|not block", ">not fold", "|+not block", "|abc", "|-2" (the indicators are out of order: indentation must come before chomping). Value f is a block string, because the comment is stripped before the header is interpreted.
Base Indent
The body of a block string consists of the lines after the header. Content lines are identified by their indentation relative to a base indent, denoted \( I_{base} \), which is stripped from each content line.
Let \( I_{parent} \) denote the indentation level of the entry’s key (the number of leading spaces on the key’s line). \( I_{base} \) is determined in one of two ways:
Automatic Detection
When no indentation indicator is given, \( I_{base} \) is inferred from the line immediately after the header. There is no scanning or skipping:
- If that line is completely empty (no characters before the newline), inference is impossible and the parse error “cannot infer base indent from an empty line” is produced.
- Otherwise, \( I_{base} \) is the number of leading spaces on that line. A whitespace-only line is treated as content for this purpose: all of its spaces count as leading spaces, and the line itself becomes an empty line within the block.
\( I_{base} \) must satisfy \( I_{base} > I_{parent} \). If the line has \( I_{base} \le I_{parent} \), the block string has no content lines (the body is empty) and that line belongs to the outer scope. If EOF immediately follows the header, the body is empty.
key |
content starts here
key is at indentation 0, so \( I_{parent} = 0 \). The next line has 4 leading spaces, so \( I_{base} = 4 \).
An empty line immediately after the header is an error:
key |
content
This produces the error “cannot infer base indent from an empty line”. To start a block string with empty lines, use the explicit indentation indicator — no inference takes place:
key |2
content
{ "key": "\ncontent\n" }
A whitespace-only line in inference position sets \( I_{base} \) by its space count, which may then make the following lines under-indented:
key |
···
··a
(where · represents a space) The whitespace-only line has 3 spaces, so \( I_{base} = 3 \). The line ··a then has content at \( I_L = 2 < I_{base} \), producing the error “block string line has insufficient indentation”.
Explicit Indentation
When the indentation indicator \( n \) is given, the base indent is fixed relative to the key’s indentation:
\[ I_{base} = I_{parent} + n \]
This is required when the first content line should itself start with spaces, which automatic detection cannot distinguish from indentation:
key |2
indented first line
second line
\( I_{parent} = 0 \), so \( I_{base} = 2 \). The first line has 6 leading spaces; stripping 2 leaves " indented first line". The second line strips to "second line".
{ "key": " indented first line\nsecond line\n" }
Line Classification
After determining \( I_{base} \), the parser processes each subsequent line. Let \( I_L \) be the number of leading spaces on line \( L \). The cases below are checked in order; the first match applies.
-
Blank line (empty or spaces only; see Whitespace): treated as an empty line within the block, regardless of how many spaces it contains.
-
Content line (\( I_L \ge I_{base} \)): The first \( I_{base} \) spaces are stripped. The remaining characters (including any extra spaces beyond \( I_{base} \)) become the line’s content.
-
Block termination (\( I_L \le I_{parent} \)): The block ends. This line belongs to the outer scope and is not part of the block string.
-
Insufficient indentation error (\( I_{parent} < I_L < I_{base} \)): produces the error “block string line has insufficient indentation”.
Tabs have no special role in this classification: \( I_L \) counts leading spaces only, and the general tab rule does not apply within the body. A tab on a content line — even immediately after the base indent — is literal content. A line whose leading spaces stop short of \( I_{base} \) because of a tab falls under case 3 or 4 like any other line.
The block also ends at EOF. Blank lines never terminate the block: trailing empty lines before the terminating line (or EOF) belong to the block, and the chomping indicator decides how they appear in the output.
Comments are never recognized within the block — # is a literal character on every line of the body:
key |
line 1
# not a comment
line 2 # also not a comment
{ "key": "line 1\n# not a comment\nline 2 # also not a comment\n" }
Indentation Stripping Example
foo |
a
b
\( I_{parent} = 0 \), \( I_{base} = 2 \) (the line after the header, a, has 2 leading spaces).
- Line
a: \( I_L = 2 \ge 2 \). Strip 2 spaces → content"a". - Line
b: \( I_L = 3 \ge 2 \). Strip 2 spaces → content" b"(the extra space is preserved).
Result (literal style, default chomp): "a\n b\n".
Nested Block Strings
When a block string appears inside a prefix block, \( I_{parent} \) is the indentation of the entry’s key within the prefix block.
section.
desc |
block line
other value
Here desc is at indentation 2, so \( I_{parent} = 2 \). The line after the header, block line, has 4 leading spaces, so \( I_{base} = 4 \). The line other value has \( I_L = 2 = I_{parent} \), so the block ends and other value is a separate entry within section..
Empty Lines Within a Block
After \( I_{base} \) has been determined, empty lines (containing only a newline) and whitespace-only lines (containing only spaces followed by a newline) within the block are preserved as empty lines in the output. The number of spaces on a whitespace-only line is irrelevant — it is an empty line whether it has fewer or more spaces than \( I_{base} \).
foo |
a
b
The completely empty line between a and b is an empty line in the output. Result: "a\n\nb\n".
foo |
··a
·····
··b
(where · represents a space) The line with five spaces is whitespace-only and treated as an empty line. Result: "a\n\nb\n".
Note that the line immediately after the header is special when automatic detection is used: it participates in base indent inference, so it must not be completely empty.
Styles
Literal Style (|)
In literal style, newlines between content lines are preserved as \n in the output.
key |
line 1
line 2
{ "key": "line 1\nline 2\n" }
Folded Style (>)
In folded style, single newlines between content lines are replaced by spaces. A sequence of two or more newlines (i.e. content separated by empty lines) preserves one newline per empty line.
text >
This is a long
sentence split
over lines.
New paragraph.
{ "text": "This is a long sentence split over lines.\nNew paragraph.\n" }
(Similar to YAML’s folded block scalar.)
Fold Suppression for More-Indented Lines
Lines whose stripped content starts with spaces (i.e. lines with indentation beyond \( I_{base} \)) suppress folding. The newline before and after a more-indented line is preserved as a literal \n, not replaced by a space.
key >
a
b
c
d
e
After stripping \( I_{base} = 2 \) spaces, the lines are: "a", "b", " c", "d", "e". Line " c" starts with spaces (more-indented), so:
- The newline between
"b"and" c"is preserved (not folded). - The newline between
" c"and"d"is preserved (not folded). - Adjacent non-indented lines (
"a"/"b"and"d"/"e") are folded as normal.
{ "key": "a b\n c\nd e\n" }
Chomping Indicators
Chomping indicators control how trailing newlines at the end of the block string are handled during evaluation:
Clip (default, no indicator)
All trailing empty lines are removed, then exactly one newline is appended.
key |
hello
world
{ "key": "hello\nworld\n" }
The trailing empty line in the source is removed during clip, and a single \n is appended.
Strip (-)
All trailing newlines are removed. No final newline is appended.
key |-
hello
world
{ "key": "hello\nworld" }
Keep (+)
All trailing empty lines are preserved.
key |+
line
foo bar
The two empty lines after line (before the block ends at foo bar) are all preserved:
{ "key": "line\n\n\n" }
The block ends when foo bar appears at \( I_L = 0 = I_{parent} \).
Prefix Blocks
Prefix blocks group entries under a common key prefix. They are a syntactic convenience that does not introduce nested objects in the evaluated output.
Syntax
A prefix block consists of:
- A key (word key or quoted key) alone on its line — the opener. After comment stripping, nothing but the key may remain on the line.
- A body of items (entries, nested prefix blocks, block strings), each indented deeper than the opener’s key.
server.
host localhost
port 8080
{
"server.host": "localhost",
"server.port": 8080
}
Opener recognition
A key alone on a line is recognized as a block opener when the next item line — skipping blank lines and comment-only lines — is indented deeper than the key:
server.
# comment lines do not affect recognition
host localhost
If the next item line is not deeper (same indentation, shallower, or EOF), the key is not an opener and the parse error “missing value for the key” is produced (see Keys):
lonely
next 1
There is no syntax for an empty block: a prefix block always contains at least one item. (A prefix block contributes no output of its own, so an empty block would produce nothing anyway.)
An inline comment after the key does not prevent opener recognition:
server. # the web server
host localhost
Body Indentation
The first item of the body determines the body indent \( I_{body} \), which must be strictly greater than the opener key’s indentation \( I_{key} \). Every subsequent item of the body must be indented at exactly \( I_{body} \).
The amount of indentation is free (any number of spaces ≥ 1 deeper than the opener) and is chosen independently for each block.
An item line ends the block when its indentation is \( \le I_{key} \). The indentation of such a line must match the body indent of one of the enclosing blocks (or column 0 for the top level); otherwise the parse error “indentation does not match any enclosing block” is produced.
a.
b.
c 1
x 2
The line x 2 at column 0 closes both b. and a..
{
"a.b.c": 1,
"x": 2
}
a.
x 1
y 2
The line y 2 has indentation 2, which matches neither the body indent of a. (4) nor the top level (0) — this is a parse error.
A line indented deeper than \( I_{body} \), where the preceding item is not a block opener (and not a block string header), is also a parse error: “unexpected indentation”.
a 1
b 2
a 1 is an entry, not an opener, so the indented b 2 is a parse error.
Blank lines and comment-only lines may appear at any indentation and never open or close a block:
server.
host localhost
# a comment at column 0 does not close the block
port 8080
{
"server.host": "localhost",
"server.port": 8080
}
Prefix Concatenation
During evaluation, the key of the prefix block is prepended to each key inside the block. No separator character (such as .) is automatically inserted. The concatenation is a simple string join.
server
.host localhost
.port 8080
The inner keys are .host and .port. Prepending server yields server.host and server.port.
{
"server.host": "localhost",
"server.port": 8080
}
Equivalently, the dot can be written on the opener instead:
server.
host localhost
port 8080
If neither side provides a separator, the prefix is joined directly:
http_
port 80
{ "http_port": 80 }
This is equivalent to writing http_port 80 at the top level.
Nesting
Prefix blocks can be nested: an opener inside a block body introduces a deeper body. The prefixes accumulate from the outermost block inward.
outer
inner
key value
The key key is inside inner, which is inside outer. The accumulated key is outerinnerkey.
{ "outerinnerkey": "value" }
To get dotted keys, include the dots explicitly:
a.
b.
c value
{ "a.b.c": "value" }
Entries and nested blocks can be mixed freely within a body:
server.
host localhost
tls.
cert /etc/ssl/cert.pem
port 8080
{
"server.host": "localhost",
"server.tls.cert": "/etc/ssl/cert.pem",
"server.port": 8080
}
Blocks With Various Value Types
Entries inside prefix blocks support all value types — Line Strings, Integers, Booleans, Quoted Strings, and Block Strings.
block.
str hello world
num 42
flag true
neg -1
quoted "value"
text |
multi
line
{
"block.str": "hello world",
"block.num": 42,
"block.flag": true,
"block.neg": -1,
"block.quoted": "value",
"block.text": "multi\nline\n"
}
For block strings inside a prefix block, the parent indentation \( I_{parent} \) is the indentation of the entry’s key within the body — see Nested Block Strings.