Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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 defined by braces { }, not by indentation.

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, 42 are 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

Lines starting with # followed by a space (or nothing) are comments. There are no inline comments.

# This is a comment
key value # this is NOT a comment, it is part of the value
{
  "key": "value # this is NOT a comment, it is part of the value"
}

Prefix Blocks

Blocks group entries under a common key prefix. They 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.

A { is only recognized as starting a block when it is the last non-whitespace character on the line. Otherwise it is part of the value.

data { port 80 }
{
  "data": "{ port 80 }"
}

Block Strings

Multi-line string values use the | (literal) or > (folded) header. Indentation of the first content line defines the base indent, which is stripped.

description |
    MICAL is simple.
    It keeps your config clean.
{
  "description": "MICAL is simple.\nIt keeps your config clean.\n"
}

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, 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, the base indent detection algorithm, line classification, styles, and chomping indicators.
  • Prefix Blocks: Block syntax, opening/closing 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 and the line is skipped.
  • Newline (U+000A): terminates lines and entries.

All other characters are non-whitespace and form part of keys or values.

Indentation

Indentation must consist of spaces only. It is cosmetic in most contexts (entries, prefix blocks), but semantic inside Block Strings where it determines content boundaries.

Structure of a Source File

A MICAL file consists of an optional shebang line followed by a sequence of items. Each item is one of:

  1. Entry — a key-value pair on a single line.
  2. Prefix Block — a key followed by {, a body of items, and a closing }. See Prefix Blocks.
  3. Comment — begins with # followed by a space, newline, or EOF.
  4. Directive — begins with # immediately followed by a word (no space).

Blank lines (empty or whitespace-only) are ignored between items.

Comments

A # at the beginning of a line (after optional indentation) starts a comment if it is followed by a space (# ...), a newline, or EOF.

# This is a comment
#

There are no inline comments. Within a value, the # character is literal:

key value # this is part of the value
{ "key": "value # this is part of the value" }

Directives

A # at the beginning of a line (before any indentation) immediately followed by a word (no space between # and the word) is parsed as a directive.

#include path/to/file
#version 1.0

A directive consists of a name (the word after #) and arguments (the rest of the line, parsed as a Line String). It is syntactically distinct from a comment because there is no space between # and the first character.

Directives are semantically equivalent to comments from the language’s perspective — they do not affect the key-value output. They exist to allow tooling (formatters, LSPs) and applications to embed meta-information:

#format disable
#scheme https://example.com/schema.json

An application that wants to support features like file inclusion can do so via directives (e.g. #include), but this is not part of the core language semantics.

Note: a # that appears after indentation (spaces) at the start of a line is always treated as a comment, never as a directive, even if it is immediately followed by a word.

  #indented_word value

This line is a comment, not a directive.

Keys

A key identifies a configuration entry. In an entry, the key is followed by whitespace and then a value. In a prefix block, the key is followed by whitespace and then {.

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.

The set of characters that can start a word key is broad: letters, digits, punctuation marks such as ., -, +, |, >, {, }, and essentially any character that is not whitespace or a quote. 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. This means braces embedded within other characters are part of the key:

foo{    value
a{b     value

These produce the keys foo{ and a{b.

Word key errors

A key alone on a line with no value is a parse error:

lonely

This produces the error: “missing value for the key”.

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. 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.

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”. The characters after the closing quote are discarded, and if a valid value follows a space, it is still parsed.

An unclosed quoted key (where the closing quote is missing before the end of the line) produces the error: “missing closing quote”. Since the entire line is consumed into the key, no value is present and an additional error “missing value for the key” is produced.

"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 examines the remaining tokens on the line to decide which value type to produce. The algorithm is:

  1. If the first token is a double or single quote (" or '), parse a Quoted String.
  2. If the first token is | or >:
    • If, after an optional chomping indicator (+ or -), the rest of the line is blank (only whitespace or newline/EOF), parse a Block String.
    • Otherwise, fall through to Line String.
  3. If the first token is true or false and the rest of the line is blank, parse a Boolean.
  4. If the first token is a numeral and the rest of the line is blank, parse an Integer.
  5. 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).
  6. Otherwise, parse a Line String (fallback).

“The rest of the line is blank” means the next token is a newline, EOF, or a single trailing space followed by newline/EOF.

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:

SequenceResult
\\Literal backslash
\"Double quote
\'Single quote
\nNewline (LF)
\rCarriage return (CR)
\tTab

All six sequences are recognized regardless of the quoting style (single or double). Any other character following a backslash is an error.

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. If any non-whitespace content appears after the closing quote, the error “unexpected token after value” is produced:

key "value" extra

This is a parse error.

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

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

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 (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 all characters literally, including #, quotes, braces, and any other punctuation:

a hello # not a comment
b { port 80 }
c value "quoted" text
{
  "a": "hello # not a comment",
  "b": "{ port 80 }",
  "c": "value \"quoted\" text"
}

Trailing spaces

For all value types, a trailing space before the newline is 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>[chomp]
    content line 1
    content line 2

The header consists of:

  • Style indicator (required): | for literal style, > for folded style.
  • Chomping indicator (optional): + (keep), - (strip), or omitted (clip).

After the style and optional chomping indicator, only whitespace (trailing spaces) and a newline (or EOF) may appear. 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 > text after

All five values above are Line Strings: "|not block", ">not fold", "|+not block", "|abc", "> text after".

Base Indent Detection

The body of a block string starts on the line after the header. The parser scans forward to find the first line with content (skipping empty lines and whitespace-only lines). The indentation of that first content line (number of leading spaces) is the base indent, denoted \( I_{base} \).

Let \( I_{parent} \) denote the indentation level of the entry’s key (the number of leading spaces on the key’s line).

\( I_{base} \) must satisfy \( I_{base} > I_{parent} \). If the first content line has \( I_{base} \le I_{parent} \), the block string has no content lines (the body is empty) and parsing stops.

If no content line exists (only empty lines or EOF follow the header), the block string has an empty body.

Example

key |
    content starts here

key is at indentation 0, so \( I_{parent} = 0 \). The first content line has 4 leading spaces, so \( I_{base} = 4 \).

Line Classification

After determining \( I_{base} \), the parser processes each subsequent line. Let \( I_L \) be the number of leading spaces on line \( L \).

  1. 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.

  2. 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.

  3. Whitespace-only line (\( I_{parent} < I_L < I_{base} \) and no non-space content after the spaces): treated as an empty line within the block.

  4. Insufficient indentation error (\( I_{parent} < I_L < I_{base} \) and the line has non-space content): produces the error “block string line has insufficient indentation”.

  5. Completely empty line (no characters before the newline): treated as an empty line within the block.

  6. Non-space at column 0: The block ends (equivalent to case 2 with \( I_L = 0 \)).

  7. Tab encountered: Because tab indentation is globally forbidden, a tab at the start of a line terminates the block and produces an error.

Continuation condition

After processing a content line or an empty line, the parser checks whether the block continues by peeking at the next line:

  • If the next line is empty (newline immediately), the block continues.
  • If the next line starts with spaces and has \( I_L > I_{parent} \), the block continues (this covers both content lines and error lines).
  • If the next line starts with non-space content at column 0, or has \( I_L \le I_{parent} \), the block ends.
  • If EOF follows, the block ends.

Indentation Stripping Example

foo |
  a
   b

\( I_{parent} = 0 \), \( I_{base} = 2 \) (first content line a has 2 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 first content line block line has \( I_{base} = 4 \). The line other value has \( I_L = 2 = I_{parent} \), so the block ends and other value is a separate entry.

Empty Lines Within a Block

Empty lines (containing only a newline, or only spaces followed by a newline) within the block are preserved as empty lines in the output. Even whitespace-only lines with fewer spaces than \( I_{base} \) (but more than \( I_{parent} \)) are treated as empty lines, not errors.

foo |

  a

The completely empty line before ··a is an empty line in the output (where · represents a space). Result: "\na\n".

foo |
·
··a

The line with a single space (·, \( I_L = 1 \), which satisfies \( 0 < 1 < 2 \) and is whitespace-only) is also treated as an empty line. Result: "\na\n".

foo |
···
··a

The line with three spaces (···, \( I_L = 3 \)) satisfies \( I_L \ge I_{base} = 2 \). After stripping \( I_{base} \) spaces, one space remains, but the line is still whitespace-only (the remaining space is followed by a newline). This is treated as an empty line. Result: "\na\n".

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:

  1. A key (word key or quoted key).
  2. A space separator.
  3. An opening brace {.
  4. The rest of the line after { must be blank (only optional whitespace followed by a newline or EOF). If { is followed by non-whitespace content on the same line, the entire thing is parsed as a regular entry with a Line String value — not as a block.
  5. A body of items (entries, nested prefix blocks, comments, directives).
  6. A closing brace } on its own line, optionally preceded and followed by whitespace.
section {
  key value
}

Opening brace recognition

The { is recognized as a block opener only when it is the sole non-whitespace character remaining on the line after the key separator. If anything else follows on the same line, the { is part of the value.

a { port 80 }

This is an entry with key a and Line String value "{ port 80 }".

a {not a block

This is an entry with key a and Line String value "{not a block".

a {
  key value
}

This is a prefix block because { is followed only by a newline.

A { with trailing spaces before the newline is also recognized:

section {··
  key value
}

(where ·· represents spaces) — this is still a prefix block.

Closing brace recognition

The closing } is recognized when it appears on a line by itself (optionally surrounded by whitespace). Specifically, the parser checks whether the line consists of optional leading spaces, a }, and then only whitespace until the newline or EOF.

If } appears on a line with other content, it is treated as a regular key, not as a closing brace:

section {
  } value
}

Here } value is an entry within the block (key }, value "value"), and the standalone } on the last line closes the block.

Missing closing brace

If EOF is reached before a closing } is found, the error “missing closing ‘}’ for prefix block” is produced. The block is still included in the AST with a missing close brace.

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
}

If the inner keys do not start with ., 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. 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" }

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"
}
{
  "blockstr": "hello world",
  "blocknum": 42,
  "blockflag": true,
  "blockneg": -1,
  "blockquoted": "value"
}

Empty Blocks

A prefix block with no entries is valid:

empty {
}

This produces no key-value entries in the output.