1
0
Fork 0
INI-inspired hardcore buttered up data file format!
Find a file
2026-03-26 13:22:32 -05:00
lobster Eliminate clone step in include processing (1.6-3.1x faster) 2026-03-26 13:03:30 -05:00
tests Add include resolution benchmarks 2026-03-26 12:57:35 -05:00
readme.md Add readme 2026-03-26 13:22:32 -05:00
spec.md Implement Lobster configuration format parser as Odin library 2026-03-26 12:04:19 -05:00

Lobster

A configuration format and parser for Odin. Hierarchical, INI-inspired, boring on purpose.

[server]
	host = "localhost"
	port = 8080

	[tls]
		enabled = true
		cert = "/etc/ssl/cert.pem"

[logging]
	level = "info"

Lobster exists because flat INI breaks down as configs grow, YAML is a footgun factory, JSON hates comments, and TOML's dotted keys hide structure inside strings. Lobster makes the hierarchy visible.

Format

Sections nest via indentation. Tabs are canonical; spaces (consistently sized) are accepted.

[database]
	name = "myapp"

	[primary]
		host = "db1.internal"
		port = 5432

	[replica]
		host = "db2.internal"
		port = 5432

Values are typed: strings ("hello"), integers (42), floats (3.14), booleans (true / false), and null.

Comments use // for line comments and /* */ for blocks.

Repeated sections with the same name become arrays:

[route]
	path = "/api"
	handler = "api_handler"

[route]
	path = "/health"
	handler = "health_check"

Multiline strings are supported. The closing quote's indentation sets the baseline:

[motd]
	text = "
		Welcome to the server.
		Please be nice.
	"

Includes pull in other files, with later values overriding earlier ones:

include "defaults.lob"

[server]
	port = 9000

See spec.md for the full specification.

Usage

import lob "lobster"

main :: proc() {
	doc, err := lob.parse_file("config.lob")
	if err.kind != .None {
		fmt.eprintln(lob.format_error(err))
		lob.destroy_error(err)
		return
	}
	defer lob.destroy(doc)

	host, _ := lob.get_string(doc, "server.host")
	port, _ := lob.get_int(doc, "server.port")
	tls, _  := lob.get_bool(doc, "server.tls.enabled")

	fmt.printf("Listening on %s:%d (tls: %v)\n", host, port, tls)
}

Querying

Dotted paths navigate the section hierarchy:

// Typed getters
name, ok := lob.get_string(doc, "database.name")
port, ok := lob.get_int(doc, "database.primary.port")
flag, ok := lob.get_bool(doc, "server.tls.enabled")
val,  ok := lob.get_float(doc, "tuning.ratio")

// Existence check
if lob.has(doc, "server.tls.enabled") { ... }

// Generic value
v, ok := lob.get(doc, "some.key")

// Repeated sections as arrays
replicas := lob.get_array(doc, "database.replica")
defer delete(replicas)

Formatting

Re-emit a document in canonical form (tabs, normalized spacing, comments stripped):

output := lob.format(doc)
defer delete(output)

Parsing from a string

doc, err := lob.parse(source, "config.lob", file_reader)

The file_reader callback is only needed if your config uses include directives.

Errors

Errors carry source location and produce readable messages:

config.lob:5:2: mixed tabs and spaces are not allowed
config.lob:12:3: duplicate key "port" in section "server"

Errors with allocated = true must be cleaned up with lob.destroy_error(err).

License

See LICENSE.