config/src/main.nika

// Parses an INI-style configuration file and prints it back, section by
// section, in the order it was written.
//
//     nikaia run -- tests/two-sections.in/app.conf
//     2 sections
//     [server]
//       host = 0.0.0.0
//       port = 8080
//     …
//
// The input is `[section]` headers, `key = value` lines under them, and `#`
// comments anywhere, such as `[server]` then `port = 8080  # the usual one`.
//
// Shows: a `grammar` that defines its own whitespace, a repetition that
// collects into a `Vec`, and a result tree whose strings are all slices of
// the input.
//
// A package: `nikaia run` in this directory runs it, and `nikaia test` checks
// what it prints against `tests/` (ADR-269 D13).

use std::fs
use std::cli

grammar Config {

    // `WS` is what a syntactic (lowercase) rule skips between its parts.
    // Defining it here to include `COMMENT` makes a comment legal wherever a
    // blank is, and no other rule has to mention comments.
    rule WSE = multispace1 -> { }
    rule WS = (WSE | COMMENT)* -> { }
    rule COMMENT = "#" until(line_ending) -> { }

    // A header and the settings under it. `setting*` stops by itself at the
    // next `[`, because a setting starts with a name and `[` is not one.
    rule section -> Section =
        "[" name:NAME "]" settings:setting* -> { Section { name, settings } }

    rule setting -> Setting =
        key:NAME "=" value:VALUE -> { Setting { key, value } }

    rule NAME -> String = s:raw_ident -> { s }

    // Lexical (uppercase), so blanks inside a value are kept. The value ends at
    // the end of the line or at a `#`, and the action trims the blanks left
    // before a comment. A `#` can therefore not appear inside a value.
    rule VALUE -> String = s:until("#" | line_ending) -> { s.trim() }

    entry rule file -> Vec[Section] =
        sections:section* -> { sections }
}

pub struct Setting {
    key: String,
    value: String,
}

// `Section` holds `Setting`s, which hold slices of the input, so a `Section`
// may not outlive the input either. The compiler infers that; nothing here
// declares it.
pub struct Section {
    name: String,
    settings: Vec[Setting],
}

fn main() throws {
    let path = cli::args().nth(1) ?? "app.conf"

    // A configuration file is small, so it is read into a string.
    let text = fs::read_to_string(ref path, fs::Root::Anywhere) catch {
        eprintln(f"cannot read {path}: {error}")
        return
    }

    let sections = Config::file(text) catch {
        eprintln(f"{path}: {error}")
        return
    }

    println(f"{sections.len()} sections")

    for section in sections {
        println(f"[{section.name}]")
        for setting in section.settings {
            println(f"  {setting.key} = {setting.value}")
        }
    }
}

The file itself · all the examples

This site is open source. Improve this page.