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