// 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}") } } }