···56(** INI parser and encoder using bytesrw.
78- Implements Python configparser semantics including:
9- - Multiline values via indentation
10- - Basic interpolation: [%(name)s]
11- - Extended interpolation: [$\{section:name\}]
12- - DEFAULT section inheritance
13- - Case-insensitive option lookup
1415- See notes about {{!layout}layout preservation}. *)
00000000000000000000000000000000000000000000000000000000001617open Bytesrw
1819-(** {1:config Configuration} *)
0002021type interpolation =
22- | No_interpolation (** RawConfigParser behavior. *)
23- | Basic_interpolation (** [%(name)s] syntax. *)
24- | Extended_interpolation (** [$\{section:name\}] syntax. *)
25-(** The type for interpolation modes. *)
0000000000000000000000000000000000000002627type config = {
28 delimiters : string list;
29- (** Key-value delimiters. Default: [["="; ":"]]. *)
00000003031 comment_prefixes : string list;
32- (** Full-line comment prefixes. Default: [["#"; ";"]]. *)
0003334 inline_comment_prefixes : string list;
35- (** Inline comment prefixes (require preceding whitespace).
36- Default: [[]] (disabled). *)
000000003738 default_section : string;
39- (** Name of the default section. Default: ["DEFAULT"]. *)
00004041 interpolation : interpolation;
42- (** Interpolation mode. Default: {!Basic_interpolation}. *)
004344 allow_no_value : bool;
45- (** Allow options without values. Default: [false]. *)
0000000004647 strict : bool;
48- (** Error on duplicate sections/options. Default: [true]. *)
0000004950 empty_lines_in_values : bool;
51- (** Allow empty lines in multiline values. Default: [true]. *)
000000000052}
53-(** The type for parser configuration. *)
05455val default_config : config
56-(** [default_config] is the default configuration matching Python's
57- [configparser.ConfigParser]. *)
0000000005859val raw_config : config
60-(** [raw_config] is configuration with no interpolation, matching
61- Python's [configparser.RawConfigParser]. *)
000006263-(** {1:decode Decode} *)
06465val decode :
66 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
67 'a Init.t -> Bytes.Reader.t -> ('a, string) result
68-(** [decode codec r] decodes a value from [r] according to [codec].
069 {ul
70- {- [config] is the parser configuration. Defaults to {!default_config}.}
71- {- If [locs] is [true] locations are preserved in metadata.
72- Defaults to [false].}
73- {- If [layout] is [true] whitespace is preserved in metadata.
74- Defaults to [false].}
75- {- [file] is the file path for error messages.
76- Defaults to {!Init.Textloc.file_none}.}} *)
007778val decode' :
79 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
80 'a Init.t -> Bytes.Reader.t -> ('a, Init.Error.t) result
81-(** [decode'] is like {!val-decode} but preserves the error structure. *)
00008283val decode_string :
84 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
85 'a Init.t -> string -> ('a, string) result
86-(** [decode_string] is like {!val-decode} but decodes from a string. *)
00000000008788val decode_string' :
89 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
90 'a Init.t -> string -> ('a, Init.Error.t) result
91-(** [decode_string'] is like {!val-decode'} but decodes from a string. *)
0009293-(** {1:encode Encode} *)
9495val encode :
96 ?buf:Bytes.t -> 'a Init.t -> 'a -> eod:bool -> Bytes.Writer.t ->
97 (unit, string) result
98-(** [encode codec v w] encodes [v] according to [codec] on [w].
099 {ul
100- {- [buf] is an optional buffer for writing.}
101- {- [eod] indicates whether to write end-of-data.}} *)
00000102103val encode' :
104 ?buf:Bytes.t -> 'a Init.t -> 'a -> eod:bool -> Bytes.Writer.t ->
105 (unit, Init.Error.t) result
106-(** [encode'] is like {!val-encode} but preserves the error structure. *)
107108val encode_string :
109 ?buf:Bytes.t -> 'a Init.t -> 'a -> (string, string) result
110-(** [encode_string] is like {!val-encode} but writes to a string. *)
00000000000000111112val encode_string' :
113 ?buf:Bytes.t -> 'a Init.t -> 'a -> (string, Init.Error.t) result
114-(** [encode_string'] is like {!val-encode'} but writes to a string. *)
000115116-(** {1:layout Layout preservation}
000117118- When [layout:true] is passed to decode functions, whitespace and
119- comments are preserved in {!Init.Meta.t} values. This enables
120- layout-preserving round-trips where the original formatting is
121- maintained as much as possible. *)
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···56(** INI parser and encoder using bytesrw.
78+ This module provides functions to parse and encode INI files using the
9+ {{:https://erratique.ch/software/bytesrw}bytesrw} streaming I/O library.
10+ It implements {{:https://docs.python.org/3/library/configparser.html}Python's
11+ configparser} semantics for maximum compatibility.
12+13+ {1:basic_usage Basic Usage}
1415+ {@ocaml[
16+ (* Define your configuration type and codec *)
17+ let config_codec = Init.Document.(
18+ obj (fun server -> server)
19+ |> section "server" server_codec ~enc:Fun.id
20+ |> finish
21+ )
22+23+ (* Decode from a string *)
24+ match Init_bytesrw.decode_string config_codec ini_text with
25+ | Ok config -> (* use config *)
26+ | Error msg -> (* handle error *)
27+28+ (* Encode back to a string *)
29+ match Init_bytesrw.encode_string config_codec config with
30+ | Ok text -> (* write text *)
31+ | Error msg -> (* handle error *)
32+ ]}
33+34+ {1:python_compat Python Compatibility}
35+36+ This parser implements the same semantics as Python's [configparser] module.
37+ Configuration files that work with Python will work here, and vice versa.
38+39+ {2:syntax Supported Syntax}
40+41+ {@ini[
42+ # Comments start with # or ;
43+ ; This is also a comment
44+45+ [section]
46+ key = value
47+ key2 : value2 ; Both = and : are delimiters
48+ key3=no spaces needed
49+50+ [multiline]
51+ long_value = This is a long value
52+ that continues on indented lines
53+ for as long as needed
54+55+ [types]
56+ integer = 42
57+ float = 3.14
58+ boolean = yes ; Also: true, on, 1, no, false, off, 0
59+ list = a, b, c, d
60+ ]}
61+62+ {2:edge_cases Edge Cases and Gotchas}
63+64+ {ul
65+ {- {b Section names are case-sensitive}: [[Server]] and [[server]] are
66+ different.}
67+ {- {b Option names are case-insensitive}: [Port] and [port] are the same.}
68+ {- {b Whitespace is trimmed} from keys and values automatically.}
69+ {- {b Empty values are allowed}: [key =] gives an empty string.}
70+ {- {b Comments are NOT preserved} during round-trips (matching Python).}
71+ {- {b Inline comments are disabled by default}: [key = value ; comment]
72+ gives the value ["value ; comment"] unless you configure
73+ {!field-inline_comment_prefixes}.}} *)
7475open Bytesrw
7677+(** {1:config Parser Configuration}
78+79+ Configure the parser to match different INI dialects. The default
80+ configuration matches Python's [ConfigParser]. *)
8182type interpolation =
83+ [ `No_interpolation
84+ (** No variable substitution. Values like ["%(foo)s"] are returned
85+ literally. Equivalent to Python's [RawConfigParser].
86+87+ Use this for configuration files that contain literal [%] or [$]
88+ characters that shouldn't be interpreted. *)
89+ | `Basic_interpolation
90+ (** Basic variable substitution using [%(name)s] syntax (default).
91+ Equivalent to Python's [ConfigParser] default.
92+93+ Variables reference options in the current section or the DEFAULT
94+ section:
95+ {@ini[
96+ [paths]
97+ base = /opt/app
98+ data = %(base)s/data ; Becomes "/opt/app/data"
99+ ]}
100+101+ {b Escaping:} Use [%%] to get a literal [%]. *)
102+ | `Extended_interpolation
103+ (** Extended substitution using [$\{section:name\}] syntax.
104+ Equivalent to Python's [ExtendedInterpolation].
105+106+ Variables can reference options in any section:
107+ {@ini[
108+ [common]
109+ base = /opt/app
110+111+ [server]
112+ data = ${common:base}/data ; Cross-section reference
113+ logs = ${base}/logs ; Same section or DEFAULT
114+ ]}
115+116+ {b Escaping:} Use [$$] to get a literal [$]. *)
117+ ]
118+(** The type for interpolation modes. Controls how variable references
119+ in values are expanded.
120+121+ {b Recursion limit:} Interpolation follows references up to 10 levels
122+ deep to prevent infinite loops. Deeper nesting raises an error.
123+124+ {b Missing references:} If a referenced option doesn't exist, decoding
125+ fails with {!Init.Error.Interpolation}. *)
126127type config = {
128 delimiters : string list;
129+ (** Characters that separate option names from values.
130+ Default: [["="; ":"]].
131+132+ The {e first} delimiter on a line is used, so values can contain
133+ delimiter characters:
134+ {@ini[
135+ url = https://example.com:8080 ; Colon in value is fine
136+ ]} *)
137138 comment_prefixes : string list;
139+ (** Prefixes that start full-line comments. Default: [["#"; ";"]].
140+141+ A line starting with any of these (after optional whitespace) is
142+ treated as a comment and ignored. *)
143144 inline_comment_prefixes : string list;
145+ (** Prefixes that start inline comments. Default: [[]] (disabled).
146+147+ {b Warning:} Enabling inline comments (e.g., [[";"]]) prevents using
148+ those characters in values. For example:
149+ {@ini[
150+ url = https://example.com;port=8080 ; Would be truncated!
151+ ]}
152+153+ A space must precede inline comments: [value;comment] keeps the
154+ semicolon, but [value ; comment] removes it. *)
155156 default_section : string;
157+ (** Name of the default section. Default: ["DEFAULT"].
158+159+ Options in this section are inherited by all other sections and
160+ available for interpolation. You can customize this, e.g., to
161+ ["general"] or ["common"]. *)
162163 interpolation : interpolation;
164+ (** How to handle variable references. Default: [`Basic_interpolation].
165+166+ See {!type-interpolation} for details on each mode. *)
167168 allow_no_value : bool;
169+ (** Allow options without values. Default: [false].
170+171+ When [true], options can appear without a delimiter:
172+ {@ini[
173+ [mysqld]
174+ skip-innodb ; No = sign, value is None
175+ port = 3306
176+ ]}
177+178+ Such options decode as [None] when using {!Init.option}. *)
179180 strict : bool;
181+ (** Reject duplicate sections and options. Default: [true].
182+183+ When [true], if the same section or option appears twice, decoding
184+ fails with {!Init.Error.Duplicate_section} or
185+ {!Init.Error.Duplicate_option}.
186+187+ When [false], later values silently override earlier ones. *)
188189 empty_lines_in_values : bool;
190+ (** Allow empty lines in multiline values. Default: [true].
191+192+ When [true], empty lines can be part of multiline values:
193+ {@ini[
194+ [section]
195+ key = line 1
196+197+ line 3 ; Empty line 2 is preserved
198+ ]}
199+200+ When [false], empty lines terminate the multiline value. *)
201}
202+(** Parser configuration. Adjust these settings to parse different INI
203+ dialects or to match specific Python configparser settings. *)
204205val default_config : config
206+(** Default configuration matching Python's [configparser.ConfigParser]:
207+208+ {ul
209+ {- [delimiters = ["="; ":"]]}
210+ {- [comment_prefixes = ["#"; ";"]]}
211+ {- [inline_comment_prefixes = []] (disabled)}
212+ {- [default_section = "DEFAULT"]}
213+ {- [interpolation = `Basic_interpolation]}
214+ {- [allow_no_value = false]}
215+ {- [strict = true]}
216+ {- [empty_lines_in_values = true]}} *)
217218val raw_config : config
219+(** Configuration matching Python's [configparser.RawConfigParser]:
220+ same as {!default_config} but with [interpolation = `No_interpolation].
221+222+ Use this when your values contain literal [%] or [$] characters. *)
223+224+225+(** {1:decode Decoding}
226227+ Parse INI data into OCaml values. All decode functions return
228+ [Result.t] - they never raise exceptions for parse errors. *)
229230val decode :
231 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
232 'a Init.t -> Bytes.Reader.t -> ('a, string) result
233+(** [decode codec r] decodes INI data from reader [r] using [codec].
234+235 {ul
236+ {- [config] configures the parser. Default: {!default_config}.}
237+ {- [locs] if [true], preserves source locations in metadata.
238+ Default: [false].}
239+ {- [layout] if [true], preserves whitespace in metadata for
240+ layout-preserving round-trips. Default: [false].}
241+ {- [file] is the file path for error messages. Default: ["-"].}}
242+243+ Returns [Ok value] on success or [Error message] on failure, where
244+ [message] includes location information when available. *)
245246val decode' :
247 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
248 'a Init.t -> Bytes.Reader.t -> ('a, Init.Error.t) result
249+(** [decode'] is like {!val-decode} but returns a structured error
250+ with separate {!Init.Error.type-kind}, location, and path information.
251+252+ Use this when you need to programmatically handle different error
253+ types or extract location information. *)
254255val decode_string :
256 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
257 'a Init.t -> string -> ('a, string) result
258+(** [decode_string codec s] decodes INI data from string [s].
259+260+ This is the most common entry point for parsing:
261+ {@ocaml[
262+ let ini_text = {|
263+ [server]
264+ host = localhost
265+ port = 8080
266+ |} in
267+ Init_bytesrw.decode_string config_codec ini_text
268+ ]} *)
269270val decode_string' :
271 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
272 'a Init.t -> string -> ('a, Init.Error.t) result
273+(** [decode_string'] is like {!val-decode_string} with structured errors. *)
274+275+276+(** {1:encode Encoding}
277278+ Serialize OCaml values to INI format. *)
279280val encode :
281 ?buf:Bytes.t -> 'a Init.t -> 'a -> eod:bool -> Bytes.Writer.t ->
282 (unit, string) result
283+(** [encode codec v ~eod w] encodes [v] to writer [w] using [codec].
284+285 {ul
286+ {- [buf] is an optional scratch buffer for writing.}
287+ {- [eod] if [true], signals end-of-data after writing.}}
288+289+ The output format follows standard INI conventions:
290+ - Sections are written as [[section_name]]
291+ - Options are written as [key = value]
292+ - Multiline values are continued with indentation *)
293294val encode' :
295 ?buf:Bytes.t -> 'a Init.t -> 'a -> eod:bool -> Bytes.Writer.t ->
296 (unit, Init.Error.t) result
297+(** [encode'] is like {!val-encode} with structured errors. *)
298299val encode_string :
300 ?buf:Bytes.t -> 'a Init.t -> 'a -> (string, string) result
301+(** [encode_string codec v] encodes [v] to a string.
302+303+ {@ocaml[
304+ let config = { server = { host = "localhost"; port = 8080 } } in
305+ match Init_bytesrw.encode_string config_codec config with
306+ | Ok text -> print_endline text
307+ | Error msg -> failwith msg
308+ ]}
309+310+ Produces:
311+ {@ini[
312+ [server]
313+ host = localhost
314+ port = 8080
315+ ]} *)
316317val encode_string' :
318 ?buf:Bytes.t -> 'a Init.t -> 'a -> (string, Init.Error.t) result
319+(** [encode_string'] is like {!val-encode_string} with structured errors. *)
320+321+322+(** {1:layout Layout Preservation}
323324+ When decoding with [~layout:true], whitespace and comment positions
325+ are preserved in the {!Init.Meta.t} values attached to each element.
326+ When re-encoding, this information is used to reproduce the original
327+ formatting as closely as possible.
328329+ {b Limitations:}
330+ {ul
331+ {- Comments are NOT preserved (matching Python's behavior).}
332+ {- Whitespace within values may be normalized.}
333+ {- The output may differ slightly from the input in edge cases.}}
334+335+ {b Performance tip:} For maximum performance when you don't need
336+ layout preservation, use [~layout:false ~locs:false] (the default).
337+ Enabling [~locs:true] improves error messages at a small cost. *)
338+339+340+(** {1:examples Examples}
341+342+ {2:simple Simple Configuration}
343+344+ {@ocaml[
345+ type config = { debug : bool; port : int }
346+347+ let codec = Init.Document.(
348+ let section = Init.Section.(
349+ obj (fun debug port -> { debug; port })
350+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug)
351+ |> mem "port" Init.int ~dec_absent:8080 ~enc:(fun c -> c.port)
352+ |> finish
353+ ) in
354+ obj Fun.id
355+ |> section "server" section ~enc:Fun.id
356+ |> finish
357+ )
358+359+ let config = Init_bytesrw.decode_string codec "[server]\nport = 9000"
360+ (* Ok { debug = false; port = 9000 } *)
361+ ]}
362+363+ {2:multi_section Multiple Sections}
364+365+ {@ocaml[
366+ type db = { host : string; port : int }
367+ type cache = { enabled : bool; ttl : int }
368+ type config = { db : db; cache : cache option }
369+370+ let db_codec = Init.Section.(
371+ obj (fun host port -> { host; port })
372+ |> mem "host" Init.string ~enc:(fun d -> d.host)
373+ |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun d -> d.port)
374+ |> finish
375+ )
376+377+ let cache_codec = Init.Section.(
378+ obj (fun enabled ttl -> { enabled; ttl })
379+ |> mem "enabled" Init.bool ~enc:(fun c -> c.enabled)
380+ |> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl)
381+ |> finish
382+ )
383+384+ let config_codec = Init.Document.(
385+ obj (fun db cache -> { db; cache })
386+ |> section "database" db_codec ~enc:(fun c -> c.db)
387+ |> opt_section "cache" cache_codec ~enc:(fun c -> c.cache)
388+ |> finish
389+ )
390+ ]}
391+392+ {2:interpolation_example Interpolation}
393+394+ {@ocaml[
395+ let paths_codec = Init.Section.(
396+ obj (fun base data logs -> (base, data, logs))
397+ |> mem "base" Init.string ~enc:(fun (b,_,_) -> b)
398+ |> mem "data" Init.string ~enc:(fun (_,d,_) -> d)
399+ |> mem "logs" Init.string ~enc:(fun (_,_,l) -> l)
400+ |> finish
401+ )
402+403+ let doc_codec = Init.Document.(
404+ obj Fun.id
405+ |> section "paths" paths_codec ~enc:Fun.id
406+ |> finish
407+ )
408+409+ (* Basic interpolation expands %(base)s *)
410+ let ini = {|
411+ [paths]
412+ base = /opt/app
413+ data = %(base)s/data
414+ logs = %(base)s/logs
415+ |}
416+417+ match Init_bytesrw.decode_string doc_codec ini with
418+ | Ok (_, data, logs) ->
419+ assert (data = "/opt/app/data");
420+ assert (logs = "/opt/app/logs")
421+ | Error _ -> assert false
422+ ]}
423+424+ {2:raw_parser Disabling Interpolation}
425+426+ {@ocaml[
427+ (* Use raw_config for files with literal % characters *)
428+ let config = Init_bytesrw.raw_config
429+430+ let result = Init_bytesrw.decode_string ~config codec {|
431+ [display]
432+ format = 100%% complete ; Would fail with basic interpolation
433+ |}
434+ ]} *)
+610-144
src/init.mli
···56(** Declarative INI data manipulation for OCaml.
78- Init provides bidirectional codecs for INI files following Python's
9- configparser semantics. The core module has no dependencies.
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001011- {b Features:}
12- - Multiline values via indentation
13- - Basic interpolation: [%(name)s]
14- - Extended interpolation: [$\{section:name\}]
15- - DEFAULT section inheritance
16- - Case-insensitive option lookup
17- - Layout preservation (whitespace and comments)
1819- {b Sub-libraries:}
20- - {!Init_bytesrw} for parsing/encoding with bytesrw
21- - {!Init_eio} for Eio file system integration *)
0000000000000000000000000000000000000000002223type 'a fmt = Format.formatter -> 'a -> unit
24-(** The type for formatters. *)
0002526-(** {1:textlocs Text Locations} *)
02728(** Text locations.
2930 A text location identifies a text span in a given file by an inclusive
31- byte position range and the start position on lines. *)
032module Textloc : sig
3334 (** {1:fpath File paths} *)
···37 (** The type for file paths. *)
3839 val file_none : fpath
40- (** [file_none] is ["-"]. A file path for when there is none. *)
04142- (** {1:pos Positions} *)
00004344 type byte_pos = int
45- (** The type for zero-based byte positions in text. *)
04647 val byte_pos_none : byte_pos
48- (** [byte_pos_none] is [-1]. A position to use when there is none. *)
4950 type line_num = int
51- (** The type for one-based line numbers. *)
5253 val line_num_none : line_num
54- (** [line_num_none] is [-1]. A line number to use when there is none. *)
5556 type line_pos = line_num * byte_pos
57- (** The type for line positions. A one-based line number and the
58- byte position of the first byte of the line. *)
005960 val line_pos_first : line_pos
61- (** [line_pos_first] is [(1, 0)]. *)
6263 val line_pos_none : line_pos
64- (** [line_pos_none] is [(line_num_none, byte_pos_none)]. *)
06566- (** {1:tlocs Text locations} *)
0006768 type t
69- (** The type for text locations. A text location identifies a text span
70- in a file by an inclusive byte position range and its line positions. *)
7172 val none : t
73- (** [none] is a text location with no information. *)
7475 val make :
76 file:fpath ->
77 first_byte:byte_pos -> last_byte:byte_pos ->
78 first_line:line_pos -> last_line:line_pos -> t
79- (** [make ~file ~first_byte ~last_byte ~first_line ~last_line] is a text
80- location with the given data. *)
0008182 val file : t -> fpath
83- (** [file l] is the file of [l]. *)
8485 val set_file : t -> fpath -> t
86- (** [set_file l f] is [l] with [file] set to [f]. *)
8788 val first_byte : t -> byte_pos
89- (** [first_byte l] is the first byte position of [l]. *)
9091 val last_byte : t -> byte_pos
92- (** [last_byte l] is the last byte position of [l]. *)
9394 val first_line : t -> line_pos
95- (** [first_line l] is the first line position of [l]. *)
9697 val last_line : t -> line_pos
98- (** [last_line l] is the last line position of [l]. *)
0099100 val is_none : t -> bool
101 (** [is_none l] is [true] iff [first_byte l < 0]. *)
102103 val is_empty : t -> bool
104- (** [is_empty l] is [true] iff [first_byte l > last_byte l]. *)
0105106 val equal : t -> t -> bool
107- (** [equal l0 l1] tests [l0] and [l1] for equality. *)
108109 val compare : t -> t -> int
110- (** [compare l0 l1] is a total order on locations. *)
000111112 val set_first : t -> first_byte:byte_pos -> first_line:line_pos -> t
113- (** [set_first l ~first_byte ~first_line] updates the first position of [l]. *)
114115 val set_last : t -> last_byte:byte_pos -> last_line:line_pos -> t
116- (** [set_last l ~last_byte ~last_line] updates the last position of [l]. *)
117118 val to_first : t -> t
119- (** [to_first l] has the start of [l] as its start and end. *)
120121 val to_last : t -> t
122- (** [to_last l] has the end of [l] as its start and end. *)
123124 val before : t -> t
125- (** [before l] is the empty location just before [l]. *)
126127 val after : t -> t
128- (** [after l] is the empty location just after [l]. *)
129130 val span : t -> t -> t
131- (** [span l0 l1] is the span from the smallest position of [l0] and [l1]
132- to the largest position of [l0] and [l1]. *)
133134 val reloc : first:t -> last:t -> t
135- (** [reloc ~first ~last] is a location that spans from [first] to [last]. *)
0136137- (** {1:fmt Formatting} *)
138139 val pp_ocaml : t fmt
140- (** [pp_ocaml] formats location using OCaml syntax. *)
0141142 val pp_gnu : t fmt
143- (** [pp_gnu] formats location using GNU syntax. *)
144145 val pp : t fmt
146 (** [pp] is {!pp_ocaml}. *)
147148 val pp_dump : t fmt
149- (** [pp_dump] formats the location for debugging. *)
150end
151152-(** {1:meta Metadata} *)
00000153154(** INI element metadata.
155156 Metadata holds text location and layout information (whitespace and
157- comments) for INI elements. This enables layout-preserving round-trips. *)
0000000158module Meta : sig
159160 type t
161 (** The type for element metadata. *)
162163 val none : t
164- (** [none] is metadata with no information. *)
165166 val make : ?ws_before:string -> ?ws_after:string -> ?comment:string ->
167 Textloc.t -> t
168- (** [make ?ws_before ?ws_after ?comment textloc] creates metadata. *)
00000169170 val is_none : t -> bool
171 (** [is_none m] is [true] iff [m] has no text location. *)
···174 (** [textloc m] is the text location of [m]. *)
175176 val ws_before : t -> string
177- (** [ws_before m] is whitespace before the element. *)
178179 val ws_after : t -> string
180- (** [ws_after m] is whitespace after the element. *)
181182 val comment : t -> string option
183- (** [comment m] is the associated comment, if any. *)
000184185 val with_textloc : t -> Textloc.t -> t
186 (** [with_textloc m loc] is [m] with text location [loc]. *)
···195 (** [with_comment m c] is [m] with [comment] set to [c]. *)
196197 val clear_ws : t -> t
198- (** [clear_ws m] clears whitespace from [m]. *)
199200 val clear_textloc : t -> t
201- (** [clear_textloc m] sets textloc to {!Textloc.none}. *)
202203 val copy_ws : t -> dst:t -> t
204 (** [copy_ws src ~dst] copies whitespace from [src] to [dst]. *)
205end
206207type 'a node = 'a * Meta.t
208-(** The type for values with metadata. *)
0209210-(** {1:paths Paths} *)
00000211212(** INI paths.
213214- Paths identify locations within an INI document, such as
215- [\[section\]/option]. *)
000000216module Path : sig
217218 (** {1:indices Path indices} *)
219220 type index =
221- | Section of string node (** A section name. *)
222- | Option of string node (** An option name. *)
223- (** The type for path indices. *)
224225 val pp_index : index fmt
226- (** [pp_index] formats an index. *)
227228 (** {1:paths Paths} *)
229230 type t
231- (** The type for paths. *)
232233 val root : t
234- (** [root] is the empty path. *)
235236 val is_root : t -> bool
237 (** [is_root p] is [true] iff [p] is {!root}. *)
···243 (** [option ?meta name p] appends an option index to [p]. *)
244245 val rev_indices : t -> index list
246- (** [rev_indices p] is the list of indices in reverse order. *)
0247248 val pp : t fmt
249- (** [pp] formats a path. *)
250end
251252-(** {1:errors Errors} *)
0000253254-(** Error handling. *)
00000000000255module Error : sig
256257- (** {1:kinds Error kinds} *)
000258259 type kind =
260 | Parse of string
0261 | Codec of string
0262 | Missing_section of string
00263 | Missing_option of { section : string; option : string }
00264 | Duplicate_section of string
00265 | Duplicate_option of { section : string; option : string }
00266 | Type_mismatch of { expected : string; got : string }
00267 | Interpolation of { option : string; reason : string }
00268 | Unknown_option of string
00269 | Unknown_section of string
00270 (** The type for error kinds. *)
271272 (** {1:errors Errors} *)
273274 type t
275- (** The type for errors. *)
0276277 val make : ?meta:Meta.t -> ?path:Path.t -> kind -> t
278 (** [make ?meta ?path kind] creates an error. *)
···281 (** [kind e] is the error kind. *)
282283 val meta : t -> Meta.t
284- (** [meta e] is the error metadata. *)
285286 val path : t -> Path.t
287- (** [path e] is the error path. *)
288289 exception Error of t
290- (** Exception for errors. *)
291292 val raise : ?meta:Meta.t -> ?path:Path.t -> kind -> 'a
293- (** [raise ?meta ?path kind] raises {!Error}. *)
00294295 val kind_to_string : kind -> string
296- (** [kind_to_string k] is a string representation of [k]. *)
297298 val to_string : t -> string
299- (** [to_string e] formats the error as a string. *)
300301 val pp : t fmt
302- (** [pp] formats an error. *)
303end
3040305(** {1:repr Internal Representations}
306307- These types are exposed for use by {!Init_bytesrw}. *)
00308module Repr : sig
309310 (** {1:values INI Values} *)
311312 type ini_value = {
313 raw : string;
0314 interpolated : string;
0315 meta : Meta.t;
0316 }
317- (** The type for decoded INI values. [raw] is the value before
318- interpolation, [interpolated] after. *)
319320 (** {1:sections INI Sections} *)
321322 type ini_section = {
323 name : string node;
0324 options : (string node * ini_value) list;
0325 meta : Meta.t;
0326 }
327 (** The type for decoded INI sections. *)
328···330331 type ini_doc = {
332 defaults : (string node * ini_value) list;
0333 sections : ini_section list;
0334 meta : Meta.t;
0335 }
336 (** The type for decoded INI documents. *)
337338- (** {1:codec_state Codec State} *)
00339340 type 'a codec_result = ('a, Error.t) result
341 (** The type for codec results. *)
···346 known_options : string list;
347 unknown_handler : [ `Skip | `Error | `Keep ];
348 }
349- (** Section codec state. *)
0350351 type 'a document_state = {
352 decode : ini_doc -> 'a codec_result;
···357 (** Document codec state. *)
358end
359360-(** {1:codecs Codecs} *)
00000000000361362type 'a t
363(** The type for INI codecs. A value of type ['a t] describes how to
364 decode INI data to type ['a] and encode ['a] to INI data. *)
365366val kind : 'a t -> string
367-(** [kind c] is a description of the kind of values [c] represents. *)
0368369val doc : 'a t -> string
370-(** [doc c] is the documentation for [c]. *)
371372val with_doc : ?kind:string -> ?doc:string -> 'a t -> 'a t
373-(** [with_doc ?kind ?doc c] is [c] with updated kind and doc. *)
374375val section_state : 'a t -> 'a Repr.section_state option
376-(** [section_state c] returns the section decode/encode state, if [c]
377- was created with {!Section.finish}. *)
378379val document_state : 'a t -> 'a Repr.document_state option
380-(** [document_state c] returns the document decode/encode state, if [c]
381- was created with {!Document.finish}. *)
382383-(** {2:base_codecs Base Codecs} *)
0000384385val string : string t
386-(** [string] is a codec for string values. *)
000387388val int : int t
389-(** [int] is a codec for integer values. *)
0000000390391val int32 : int32 t
392-(** [int32] is a codec for 32-bit integer values. *)
393394val int64 : int64 t
395-(** [int64] is a codec for 64-bit integer values. *)
396397val float : float t
398-(** [float] is a codec for floating-point values. *)
0000399400val bool : bool t
401-(** [bool] is a codec for Python-compatible booleans.
402- Accepts (case-insensitive): [1/yes/true/on] for true,
403- [0/no/false/off] for false. *)
0000000000404405val bool_01 : bool t
406-(** [bool_01] is a strict codec for ["0"]/["1"] booleans. *)
00407408val bool_yesno : bool t
409-(** [bool_yesno] is a codec for ["yes"]/["no"] booleans. *)
410411val bool_truefalse : bool t
412-(** [bool_truefalse] is a codec for ["true"]/["false"] booleans. *)
413414val bool_onoff : bool t
415-(** [bool_onoff] is a codec for ["on"]/["off"] booleans. *)
000416417-(** {2:combinators Combinators} *)
418419val map : ?kind:string -> ?doc:string ->
420 dec:('a -> 'b) -> enc:('b -> 'a) -> 'a t -> 'b t
421-(** [map ~dec ~enc c] transforms [c] using [dec] for decoding
422- and [enc] for encoding. *)
00000000423424val enum : ?cmp:('a -> 'a -> int) -> ?kind:string -> ?doc:string ->
425 (string * 'a) list -> 'a t
426-(** [enum assoc] is a codec for enumerated values. String matching
427- is case-insensitive. *)
0000000000000428429val option : ?kind:string -> ?doc:string -> 'a t -> 'a option t
430-(** [option c] is a codec for optional values. Empty strings decode
431- to [None]. *)
0000432433val default : 'a -> 'a t -> 'a t
434-(** [default v c] uses [v] when decoding fails. *)
000000435436val list : ?sep:char -> 'a t -> 'a list t
437-(** [list ?sep c] is a codec for lists of values separated by [sep]
438- (default: [',']). *)
00000000439440(** {1:sections Section Codecs}
441442- Build codecs for INI sections using an applicative style. *)
00000000000000000000000000443module Section : sig
444445 type 'a codec = 'a t
446- (** Alias for codec type. *)
447448 type ('o, 'dec) map
449- (** The type for section maps. ['o] is the OCaml type being built,
450- ['dec] is the remaining constructor arguments. *)
0451452 val obj : ?kind:string -> ?doc:string -> 'dec -> ('o, 'dec) map
453- (** [obj f] starts building a section codec with constructor [f]. *)
0000454455 val mem : ?doc:string -> ?dec_absent:'a -> ?enc:('o -> 'a) ->
456 ?enc_omit:('a -> bool) ->
457 string -> 'a codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
458- (** [mem name c m] adds an option [name] decoded by [c] to map [m].
459- @param dec_absent Default value if option is absent.
460- @param enc Encoder function to extract value from ['o].
461- @param enc_omit Predicate; if true, omit option during encoding. *)
0000000462463 val opt_mem : ?doc:string -> ?enc:('o -> 'a option) ->
464 string -> 'a codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
465- (** [opt_mem name c m] adds an optional option (decodes to [None] if absent). *)
000000000000466467 val skip_unknown : ('o, 'dec) map -> ('o, 'dec) map
468- (** [skip_unknown m] ignores unknown options (default). *)
0469470 val error_unknown : ('o, 'dec) map -> ('o, 'dec) map
471- (** [error_unknown m] raises an error on unknown options. *)
0472473 val keep_unknown : ?enc:('o -> (string * string) list) ->
474 ('o, (string * string) list -> 'dec) map -> ('o, 'dec) map
475- (** [keep_unknown m] captures unknown options as a list of (name, value) pairs. *)
0000000000000000476477 val finish : ('o, 'o) map -> 'o codec
478- (** [finish m] completes the section codec. *)
0479end
4800481(** {1:documents Document Codecs}
482483- Build codecs for complete INI documents. *)
00000000000000000484module Document : sig
485486 type 'a codec = 'a t
487- (** Alias for codec type. *)
488489 type ('o, 'dec) map
490- (** The type for document maps. *)
491492 val obj : ?kind:string -> ?doc:string -> 'dec -> ('o, 'dec) map
493 (** [obj f] starts building a document codec with constructor [f]. *)
494495 val section : ?doc:string -> ?enc:('o -> 'a) ->
496 string -> 'a Section.codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
497- (** [section name c m] adds a required section [name] to map [m]. *)
000000498499 val opt_section : ?doc:string -> ?enc:('o -> 'a option) ->
500 string -> 'a Section.codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
501- (** [opt_section name c m] adds an optional section [name] to map [m]. *)
00502503 val defaults : ?doc:string -> ?enc:('o -> 'a) ->
504 'a Section.codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
505- (** [defaults c m] decodes the DEFAULT section using [c]. *)
0000000506507 val opt_defaults : ?doc:string -> ?enc:('o -> 'a option) ->
508 'a Section.codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
509- (** [opt_defaults c m] optionally decodes the DEFAULT section. *)
00510511 val skip_unknown : ('o, 'dec) map -> ('o, 'dec) map
512- (** [skip_unknown m] ignores unknown sections (default). *)
0513514 val error_unknown : ('o, 'dec) map -> ('o, 'dec) map
515- (** [error_unknown m] raises an error on unknown sections. *)
0516517 val finish : ('o, 'o) map -> 'o codec
518 (** [finish m] completes the document codec. *)
···56(** Declarative INI data manipulation for OCaml.
78+ Init provides bidirectional codecs for INI configuration files following
9+ {{:https://docs.python.org/3/library/configparser.html}Python's
10+ configparser} semantics. This ensures configuration files are compatible
11+ with Python tools while providing a type-safe OCaml interface.
12+13+ {1:quick_start Quick Start}
14+15+ INI files consist of sections (in square brackets) containing key-value
16+ pairs. Here's a simple configuration file:
17+18+ {@ini[
19+ [server]
20+ host = localhost
21+ port = 8080
22+ debug = false
23+24+ [database]
25+ connection_string = postgres://localhost/mydb
26+ pool_size = 10
27+ ]}
28+29+ To decode this, define an OCaml type and create a codec:
30+31+ {@ocaml[
32+ (* Define the OCaml types *)
33+ type server = { host : string; port : int; debug : bool }
34+ type database = { connection : string; pool_size : int }
35+ type config = { server : server; database : database }
36+37+ (* Create section codecs *)
38+ let server_codec = Init.Section.(
39+ obj (fun host port debug -> { host; port; debug })
40+ |> mem "host" Init.string ~enc:(fun s -> s.host)
41+ |> mem "port" Init.int ~enc:(fun s -> s.port)
42+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun s -> s.debug)
43+ |> finish
44+ )
45+46+ let database_codec = Init.Section.(
47+ obj (fun connection pool_size -> { connection; pool_size })
48+ |> mem "connection_string" Init.string ~enc:(fun d -> d.connection)
49+ |> mem "pool_size" Init.int ~dec_absent:5 ~enc:(fun d -> d.pool_size)
50+ |> finish
51+ )
52+53+ (* Create the document codec *)
54+ let config_codec = Init.Document.(
55+ obj (fun server database -> { server; database })
56+ |> section "server" server_codec ~enc:(fun c -> c.server)
57+ |> section "database" database_codec ~enc:(fun c -> c.database)
58+ |> finish
59+ )
60+61+ (* Use Init_bytesrw to decode *)
62+ let config = Init_bytesrw.decode_string config_codec ini_string
63+ ]}
64+65+ Read the {{!page-cookbook}cookbook} for more examples.
66+67+ {1:concepts Key Concepts}
68+69+ {2:ini_format The INI Format}
70+71+ An INI file is a simple text format for configuration data:
72+73+ {ul
74+ {- {b Sections} are named groups enclosed in square brackets: [[section]].
75+ Section names are case-sensitive and can contain spaces.}
76+ {- {b Options} (also called keys) are name-value pairs separated by
77+ [=] or [:]. Option names are {e case-insensitive} by default.}
78+ {- {b Values} are strings. All data is stored as strings and converted
79+ to other types (int, bool, etc.) by codecs during decoding.}
80+ {- {b Comments} start with [#] or [;] at the beginning of a line.}
81+ {- {b Multiline values} are continued on indented lines.}}
82+83+ {2:defaults_section The DEFAULT Section}
84+85+ The special [[DEFAULT]] section provides fallback values for all other
86+ sections. When an option is not found in a section, the parser looks
87+ in DEFAULT:
88+89+ {@ini[
90+ [DEFAULT]
91+ base_dir = /opt/app
92+ log_level = info
93+94+ [production]
95+ base_dir = /var/app
96+97+ [development]
98+ # Inherits base_dir from DEFAULT
99+ log_level = debug
100+ ]}
101+102+ {2:interpolation Value Interpolation}
103+104+ Values can reference other values using interpolation syntax:
105+106+ {b Basic interpolation} (Python's ConfigParser default):
107+108+ {@ini[
109+ [paths]
110+ base = /opt/app
111+ data = %(base)s/data
112+ logs = %(base)s/logs
113+ ]}
114+115+ {b Extended interpolation} (cross-section references):
116+117+ {@ini[
118+ [common]
119+ base = /opt/app
120+121+ [server]
122+ data_dir = ${common:base}/data
123+ ]}
124+125+ See {!Init_bytesrw.type-interpolation} for configuration options.
126+127+ {2:booleans Boolean Values}
128+129+ Following Python's configparser, these values are recognized as booleans
130+ (case-insensitive):
131132+ {ul
133+ {- {b True}: [1], [yes], [true], [on]}
134+ {- {b False}: [0], [no], [false], [off]}}
135+136+ Use {!bool} for Python-compatible parsing, or the stricter variants
137+ {!bool_01}, {!bool_yesno}, {!bool_truefalse}, {!bool_onoff}.
0138139+ {2:case_sensitivity Case Sensitivity}
140+141+ By default (matching Python's behavior):
142+ {ul
143+ {- Section names are {b case-sensitive}: [[Server]] and [[server]] are
144+ different sections.}
145+ {- Option names are {b case-insensitive}: [Port] and [port] refer to the
146+ same option. Options are normalized to lowercase internally.}}
147+148+ {1:architecture Library Architecture}
149+150+ The library is split into multiple packages:
151+152+ {ul
153+ {- {b init} (this module): Core types and codec combinators. No I/O
154+ dependencies.}
155+ {- {b init.bytesrw} ({!Init_bytesrw}): Parsing and encoding using
156+ {{:https://erratique.ch/software/bytesrw}bytesrw}.}
157+ {- {b init.eio}: File system integration with
158+ {{:https://github.com/ocaml-multicore/eio}Eio}.}}
159+160+ {1:error_handling Error Handling}
161+162+ All decode functions return [('a, string) result] or [('a, Error.t) result].
163+ Common error cases:
164+165+ {ul
166+ {- {!Error.Missing_section}: Required section not found in file.}
167+ {- {!Error.Missing_option}: Required option not found in section.}
168+ {- {!Error.Type_mismatch}: Value could not be converted (e.g., ["abc"]
169+ as an integer).}
170+ {- {!Error.Interpolation}: Variable reference could not be resolved.}
171+ {- {!Error.Duplicate_section}, {!Error.Duplicate_option}: In strict mode,
172+ duplicates are errors.}}
173+174+ {1:cookbook Cookbook Patterns}
175+176+ See the {{!page-cookbook}cookbook} for detailed examples:
177+178+ {ul
179+ {- {{!page-cookbook.optional_values}Optional values and defaults}}
180+ {- {{!page-cookbook.lists}Lists and comma-separated values}}
181+ {- {{!page-cookbook.unknown_options}Handling unknown options}}
182+ {- {{!page-cookbook.interpolation}Interpolation (basic and extended)}}
183+ {- {{!page-cookbook.roundtrip}Layout-preserving round-trips}}} *)
184185type 'a fmt = Format.formatter -> 'a -> unit
186+(** The type for formatters of values of type ['a]. *)
187+188+189+(** {1:textlocs Text Locations}
190191+ Text locations track where elements appear in source files. This enables
192+ good error messages and layout-preserving round-trips. *)
193194(** Text locations.
195196 A text location identifies a text span in a given file by an inclusive
197+ byte position range and the start position on lines. Use these to
198+ provide precise error messages pointing to the source. *)
199module Textloc : sig
200201 (** {1:fpath File paths} *)
···204 (** The type for file paths. *)
205206 val file_none : fpath
207+ (** [file_none] is ["-"]. A placeholder file path when there is no file
208+ (e.g., when parsing from a string). *)
209210+ (** {1:pos Positions}
211+212+ Positions identify locations within text. Byte positions are zero-based
213+ absolute offsets. Line positions combine a one-based line number with
214+ the byte position of the line's start. *)
215216 type byte_pos = int
217+ (** The type for zero-based, absolute byte positions in text. If the text
218+ has [n] bytes, [0] is the first byte and [n-1] is the last. *)
219220 val byte_pos_none : byte_pos
221+ (** [byte_pos_none] is [-1]. A sentinel value indicating no position. *)
222223 type line_num = int
224+ (** The type for one-based line numbers. The first line is line [1]. *)
225226 val line_num_none : line_num
227+ (** [line_num_none] is [-1]. A sentinel value indicating no line number. *)
228229 type line_pos = line_num * byte_pos
230+ (** The type for line positions. A pair of:
231+ {ul
232+ {- A one-based line number.}
233+ {- The absolute byte position of the first character of that line.}} *)
234235 val line_pos_first : line_pos
236+ (** [line_pos_first] is [(1, 0)]. The position of the first line. *)
237238 val line_pos_none : line_pos
239+ (** [line_pos_none] is [(line_num_none, byte_pos_none)]. Indicates no
240+ line position. *)
241242+ (** {1:tlocs Text locations}
243+244+ A text location spans from a first position to a last position
245+ (inclusive), tracking both byte offsets and line numbers. *)
246247 type t
248+ (** The type for text locations. A text location identifies a span of
249+ text in a file by its start and end positions. *)
250251 val none : t
252+ (** [none] is a location with no information. Use {!is_none} to test. *)
253254 val make :
255 file:fpath ->
256 first_byte:byte_pos -> last_byte:byte_pos ->
257 first_line:line_pos -> last_line:line_pos -> t
258+ (** [make ~file ~first_byte ~last_byte ~first_line ~last_line] creates
259+ a text location spanning from [first_byte] to [last_byte] in [file].
260+ The line positions provide human-readable context.
261+262+ Use {!file_none} if there is no file (e.g., parsing from a string). *)
263264 val file : t -> fpath
265+ (** [file l] is the file path of [l]. *)
266267 val set_file : t -> fpath -> t
268+ (** [set_file l f] is [l] with the file set to [f]. *)
269270 val first_byte : t -> byte_pos
271+ (** [first_byte l] is the byte position where [l] starts (inclusive). *)
272273 val last_byte : t -> byte_pos
274+ (** [last_byte l] is the byte position where [l] ends (inclusive). *)
275276 val first_line : t -> line_pos
277+ (** [first_line l] is the line position where [l] starts. *)
278279 val last_line : t -> line_pos
280+ (** [last_line l] is the line position where [l] ends. *)
281+282+ (** {2:preds Predicates and comparisons} *)
283284 val is_none : t -> bool
285 (** [is_none l] is [true] iff [first_byte l < 0]. *)
286287 val is_empty : t -> bool
288+ (** [is_empty l] is [true] iff [first_byte l > last_byte l]. An empty
289+ location represents a position between characters. *)
290291 val equal : t -> t -> bool
292+ (** [equal l0 l1] is [true] iff [l0] and [l1] are equal. *)
293294 val compare : t -> t -> int
295+ (** [compare l0 l1] is a total order on locations, comparing by file,
296+ then start position, then end position. *)
297+298+ (** {2:transforms Transformations} *)
299300 val set_first : t -> first_byte:byte_pos -> first_line:line_pos -> t
301+ (** [set_first l ~first_byte ~first_line] updates the start position. *)
302303 val set_last : t -> last_byte:byte_pos -> last_line:line_pos -> t
304+ (** [set_last l ~last_byte ~last_line] updates the end position. *)
305306 val to_first : t -> t
307+ (** [to_first l] is a zero-width location at the start of [l]. *)
308309 val to_last : t -> t
310+ (** [to_last l] is a zero-width location at the end of [l]. *)
311312 val before : t -> t
313+ (** [before l] is an empty location immediately before [l]. *)
314315 val after : t -> t
316+ (** [after l] is an empty location immediately after [l]. *)
317318 val span : t -> t -> t
319+ (** [span l0 l1] is the smallest location containing both [l0] and [l1]. *)
0320321 val reloc : first:t -> last:t -> t
322+ (** [reloc ~first ~last] creates a location from the start of [first]
323+ to the end of [last]. *)
324325+ (** {2:fmt Formatting} *)
326327 val pp_ocaml : t fmt
328+ (** [pp_ocaml] formats locations in OCaml-style:
329+ [File "path", line N, characters M-P]. *)
330331 val pp_gnu : t fmt
332+ (** [pp_gnu] formats locations in GNU-style: [path:line:column]. *)
333334 val pp : t fmt
335 (** [pp] is {!pp_ocaml}. *)
336337 val pp_dump : t fmt
338+ (** [pp_dump] formats all location fields for debugging. *)
339end
340341+342+(** {1:meta Metadata}
343+344+ Metadata tracks source location and layout (whitespace/comments) for
345+ INI elements. This enables layout-preserving round-trips where your
346+ output matches your input formatting. *)
347348(** INI element metadata.
349350 Metadata holds text location and layout information (whitespace and
351+ comments) for INI elements. When decoding with [~layout:true], this
352+ information is captured and can be used to preserve formatting when
353+ re-encoding.
354+355+ {b Example:} A value decoded from:
356+ {[ port = 8080 # server port]}
357+ would have [ws_before = " "], [ws_after = " "], and
358+ [comment = Some "# server port"]. *)
359module Meta : sig
360361 type t
362 (** The type for element metadata. *)
363364 val none : t
365+ (** [none] is metadata with no information (no location, no whitespace). *)
366367 val make : ?ws_before:string -> ?ws_after:string -> ?comment:string ->
368 Textloc.t -> t
369+ (** [make ?ws_before ?ws_after ?comment textloc] creates metadata.
370+ {ul
371+ {- [ws_before] is whitespace preceding the element.}
372+ {- [ws_after] is whitespace following the element.}
373+ {- [comment] is an associated comment (including the [#] or [;]).}
374+ {- [textloc] is the source location.}} *)
375376 val is_none : t -> bool
377 (** [is_none m] is [true] iff [m] has no text location. *)
···380 (** [textloc m] is the text location of [m]. *)
381382 val ws_before : t -> string
383+ (** [ws_before m] is whitespace that appeared before the element. *)
384385 val ws_after : t -> string
386+ (** [ws_after m] is whitespace that appeared after the element. *)
387388 val comment : t -> string option
389+ (** [comment m] is the comment associated with the element, if any.
390+ Includes the comment prefix ([#] or [;]). *)
391+392+ (** {2:with_accessors Functional updates} *)
393394 val with_textloc : t -> Textloc.t -> t
395 (** [with_textloc m loc] is [m] with text location [loc]. *)
···404 (** [with_comment m c] is [m] with [comment] set to [c]. *)
405406 val clear_ws : t -> t
407+ (** [clear_ws m] is [m] with whitespace cleared. *)
408409 val clear_textloc : t -> t
410+ (** [clear_textloc m] is [m] with textloc set to {!Textloc.none}. *)
411412 val copy_ws : t -> dst:t -> t
413 (** [copy_ws src ~dst] copies whitespace from [src] to [dst]. *)
414end
415416type 'a node = 'a * Meta.t
417+(** The type for values paired with metadata. Used internally to track
418+ source information for section and option names. *)
419420+421+(** {1:paths Paths}
422+423+ Paths identify locations within an INI document, like
424+ [[server]/port]. They are used in error messages to show where
425+ problems occurred. *)
426427(** INI paths.
428429+ Paths provide a way to address locations within an INI document.
430+ A path is a sequence of section and option indices.
431+432+ {b Example paths:}
433+ {ul
434+ {- [[server]] - the server section}
435+ {- [[server]/host] - the host option in the server section}
436+ {- [[database]/pool_size] - the pool_size option in database}} *)
437module Path : sig
438439 (** {1:indices Path indices} *)
440441 type index =
442+ | Section of string node (** A section name with metadata. *)
443+ | Option of string node (** An option name with metadata. *)
444+ (** The type for path indices. An index is either a section or option name. *)
445446 val pp_index : index fmt
447+ (** [pp_index] formats an index as [[section]] or [/option]. *)
448449 (** {1:paths Paths} *)
450451 type t
452+ (** The type for paths. A sequence of indices from root to leaf. *)
453454 val root : t
455+ (** [root] is the empty path (the document root). *)
456457 val is_root : t -> bool
458 (** [is_root p] is [true] iff [p] is {!root}. *)
···464 (** [option ?meta name p] appends an option index to [p]. *)
465466 val rev_indices : t -> index list
467+ (** [rev_indices p] is the indices of [p] in reverse order
468+ (from leaf to root). *)
469470 val pp : t fmt
471+ (** [pp] formats a path as [[section]/option]. *)
472end
473474+475+(** {1:error_module Errors}
476+477+ Error handling for INI parsing and codec operations. Errors include
478+ source location information when available. *)
479480+(** Error handling.
481+482+ The {!Error} module defines the types of errors that can occur during
483+ parsing and codec operations. All errors carry optional location
484+ information for good error messages.
485+486+ {b Error categories:}
487+ {ul
488+ {- {b Parse errors}: Malformed INI syntax.}
489+ {- {b Structure errors}: Missing or duplicate sections/options.}
490+ {- {b Type errors}: Values that cannot be converted to the expected type.}
491+ {- {b Interpolation errors}: Unresolved variable references.}} *)
492module Error : sig
493494+ (** {1:kinds Error kinds}
495+496+ Each error kind corresponds to a specific failure mode. This mirrors
497+ Python's configparser exception hierarchy. *)
498499 type kind =
500 | Parse of string
501+ (** Malformed INI syntax. The string describes the issue. *)
502 | Codec of string
503+ (** Generic codec error. *)
504 | Missing_section of string
505+ (** A required section was not found. Analogous to Python's
506+ [NoSectionError]. *)
507 | Missing_option of { section : string; option : string }
508+ (** A required option was not found in the section. Analogous to
509+ Python's [NoOptionError]. *)
510 | Duplicate_section of string
511+ (** In strict mode, a section appeared more than once. Analogous to
512+ Python's [DuplicateSectionError]. *)
513 | Duplicate_option of { section : string; option : string }
514+ (** In strict mode, an option appeared more than once. Analogous to
515+ Python's [DuplicateOptionError]. *)
516 | Type_mismatch of { expected : string; got : string }
517+ (** The value could not be converted. For example, ["abc"] when
518+ expecting an integer. *)
519 | Interpolation of { option : string; reason : string }
520+ (** Variable interpolation failed. Analogous to Python's
521+ [InterpolationError] subclasses. *)
522 | Unknown_option of string
523+ (** An option was present but not expected (when using
524+ {!Section.error_unknown}). *)
525 | Unknown_section of string
526+ (** A section was present but not expected (when using
527+ {!Document.error_unknown}). *)
528 (** The type for error kinds. *)
529530 (** {1:errors Errors} *)
531532 type t
533+ (** The type for errors. An error combines a {!type-kind} with optional
534+ location ({!Meta.t}) and path ({!Path.t}) information. *)
535536 val make : ?meta:Meta.t -> ?path:Path.t -> kind -> t
537 (** [make ?meta ?path kind] creates an error. *)
···540 (** [kind e] is the error kind. *)
541542 val meta : t -> Meta.t
543+ (** [meta e] is the error metadata (contains source location). *)
544545 val path : t -> Path.t
546+ (** [path e] is the path where the error occurred. *)
547548 exception Error of t
549+ (** Exception for errors. Raised by {!raise}. *)
550551 val raise : ?meta:Meta.t -> ?path:Path.t -> kind -> 'a
552+ (** [raise ?meta ?path kind] raises [Error (make ?meta ?path kind)]. *)
553+554+ (** {2:fmt Formatting} *)
555556 val kind_to_string : kind -> string
557+ (** [kind_to_string k] is a human-readable description of [k]. *)
558559 val to_string : t -> string
560+ (** [to_string e] formats [e] as a string with location information. *)
561562 val pp : t fmt
563+ (** [pp] formats an error for display. *)
564end
565566+567(** {1:repr Internal Representations}
568569+ These types expose the internal INI representation for use by
570+ {!Init_bytesrw} and other codec backends. Most users should use
571+ the high-level {!Section} and {!Document} modules instead. *)
572module Repr : sig
573574 (** {1:values INI Values} *)
575576 type ini_value = {
577 raw : string;
578+ (** The raw value before interpolation (e.g., ["%(base)s/data"]). *)
579 interpolated : string;
580+ (** The value after interpolation (e.g., ["/opt/app/data"]). *)
581 meta : Meta.t;
582+ (** Source location and layout. *)
583 }
584+ (** The type for decoded INI values. The [raw] field preserves the original
585+ text for round-tripping, while [interpolated] has variables expanded. *)
586587 (** {1:sections INI Sections} *)
588589 type ini_section = {
590 name : string node;
591+ (** Section name with metadata (e.g., ["server"]). *)
592 options : (string node * ini_value) list;
593+ (** List of (option_name, value) pairs in file order. *)
594 meta : Meta.t;
595+ (** Metadata for the section header line. *)
596 }
597 (** The type for decoded INI sections. *)
598···600601 type ini_doc = {
602 defaults : (string node * ini_value) list;
603+ (** Options from the DEFAULT section (available to all sections). *)
604 sections : ini_section list;
605+ (** All non-DEFAULT sections in file order. *)
606 meta : Meta.t;
607+ (** Document-level metadata. *)
608 }
609 (** The type for decoded INI documents. *)
610611+ (** {1:codec_state Codec State}
612+613+ Internal state used by section and document codecs. *)
614615 type 'a codec_result = ('a, Error.t) result
616 (** The type for codec results. *)
···621 known_options : string list;
622 unknown_handler : [ `Skip | `Error | `Keep ];
623 }
624+ (** Section codec state. The [known_options] list is used to validate
625+ that required options are present and to detect unknown options. *)
626627 type 'a document_state = {
628 decode : ini_doc -> 'a codec_result;
···633 (** Document codec state. *)
634end
635636+637+(** {1:codecs Codecs}
638+639+ Codecs describe bidirectional mappings between INI text and OCaml values.
640+ A codec of type ['a t] can:
641+ {ul
642+ {- {b Decode}: Parse INI text into an OCaml value of type ['a].}
643+ {- {b Encode}: Serialize an OCaml value of type ['a] to INI text.}}
644+645+ Base codecs handle primitive types ({!string}, {!int}, {!bool}, etc.).
646+ Compose them using {!Section} and {!Document} modules to build codecs
647+ for complex configuration types. *)
648649type 'a t
650(** The type for INI codecs. A value of type ['a t] describes how to
651 decode INI data to type ['a] and encode ['a] to INI data. *)
652653val kind : 'a t -> string
654+(** [kind c] is a short description of what [c] represents (e.g.,
655+ ["integer"], ["server configuration"]). Used in error messages. *)
656657val doc : 'a t -> string
658+(** [doc c] is the documentation string for [c]. *)
659660val with_doc : ?kind:string -> ?doc:string -> 'a t -> 'a t
661+(** [with_doc ?kind ?doc c] is [c] with updated kind and documentation. *)
662663val section_state : 'a t -> 'a Repr.section_state option
664+(** [section_state c] returns the section codec state if [c] was created
665+ with {!Section.finish}. Returns [None] for non-section codecs. *)
666667val document_state : 'a t -> 'a Repr.document_state option
668+(** [document_state c] returns the document codec state if [c] was created
669+ with {!Document.finish}. Returns [None] for non-document codecs. *)
670671+672+(** {2:base_codecs Base Codecs}
673+674+ Codecs for primitive INI value types. All INI values are strings
675+ internally; these codecs handle the conversion to/from OCaml types. *)
676677val string : string t
678+(** [string] is the identity codec. Values are returned as-is.
679+680+ {b Example:}
681+ - ["hello world"] decodes to ["hello world"]. *)
682683val int : int t
684+(** [int] decodes decimal integers.
685+686+ {b Example:}
687+ - ["42"] decodes to [42]
688+ - ["-100"] decodes to [-100]
689+ - ["0xFF"] fails (hex not supported)
690+691+ {b Warning.} Behavior depends on [Sys.int_size] for very large values. *)
692693val int32 : int32 t
694+(** [int32] decodes 32-bit integers. *)
695696val int64 : int64 t
697+(** [int64] decodes 64-bit integers. *)
698699val float : float t
700+(** [float] decodes floating-point numbers.
701+702+ {b Example:}
703+ - ["3.14"] decodes to [3.14]
704+ - ["1e-10"] decodes to [1e-10] *)
705706val bool : bool t
707+(** [bool] decodes Python-compatible boolean values. Matching is
708+ case-insensitive.
709+710+ {b True values:} [1], [yes], [true], [on]
711+712+ {b False values:} [0], [no], [false], [off]
713+714+ This matches Python's [configparser.getboolean()] behavior exactly.
715+716+ {b Example:}
717+ - ["yes"], ["YES"], ["Yes"] all decode to [true]
718+ - ["0"], ["no"], ["OFF"] all decode to [false]
719+ - ["maybe"] fails with a type error *)
720721val bool_01 : bool t
722+(** [bool_01] decodes only ["0"] and ["1"].
723+724+ Use this for stricter parsing when you want exactly these values. *)
725726val bool_yesno : bool t
727+(** [bool_yesno] decodes ["yes"] and ["no"] (case-insensitive). *)
728729val bool_truefalse : bool t
730+(** [bool_truefalse] decodes ["true"] and ["false"] (case-insensitive). *)
731732val bool_onoff : bool t
733+(** [bool_onoff] decodes ["on"] and ["off"] (case-insensitive). *)
734+735+736+(** {2:combinators Combinators}
737738+ Build complex codecs from simpler ones. *)
739740val map : ?kind:string -> ?doc:string ->
741 dec:('a -> 'b) -> enc:('b -> 'a) -> 'a t -> 'b t
742+(** [map ~dec ~enc c] transforms codec [c] using [dec] for decoding
743+ and [enc] for encoding.
744+745+ {b Example:} A codec for URIs:
746+ {@ocaml[
747+ let uri = Init.map
748+ ~dec:Uri.of_string
749+ ~enc:Uri.to_string
750+ Init.string
751+ ]} *)
752753val enum : ?cmp:('a -> 'a -> int) -> ?kind:string -> ?doc:string ->
754 (string * 'a) list -> 'a t
755+(** [enum assoc] creates a codec for enumerated values. String matching
756+ is case-insensitive.
757+758+ {b Example:}
759+ {@ocaml[
760+ type log_level = Debug | Info | Warn | Error
761+ let log_level = Init.enum [
762+ "debug", Debug;
763+ "info", Info;
764+ "warn", Warn;
765+ "error", Error;
766+ ]
767+ ]}
768+769+ @param cmp Comparison function for encoding (default: polymorphic compare). *)
770771val option : ?kind:string -> ?doc:string -> 'a t -> 'a option t
772+(** [option c] wraps codec [c] to handle optional values. Empty strings
773+ decode to [None]; [None] encodes to empty string.
774+775+ {b Note:} For optional INI options (that may be absent), use
776+ {!Section.opt_mem} instead. This codec is for values that are
777+ present but may be empty. *)
778779val default : 'a -> 'a t -> 'a t
780+(** [default v c] uses [v] when decoding with [c] fails.
781+782+ {b Example:}
783+ {@ocaml[
784+ let port = Init.default 8080 Init.int
785+ (* "abc" decodes to 8080 instead of failing *)
786+ ]} *)
787788val list : ?sep:char -> 'a t -> 'a list t
789+(** [list ?sep c] decodes comma-separated (or [sep]-separated) values.
790+791+ {b Example:}
792+ - ["a,b,c"] with [list string] decodes to [["a"; "b"; "c"]]
793+ - ["1, 2, 3"] with [list int] decodes to [[1; 2; 3]]
794+795+ Whitespace around elements is trimmed.
796+797+ @param sep Separator character (default: [',']). *)
798+799800(** {1:sections Section Codecs}
801802+ Build codecs for INI sections using an applicative style. A section
803+ codec maps the options in a [[section]] to fields of an OCaml record.
804+805+ {2:section_pattern The Pattern}
806+807+ {@ocaml[
808+ type server = { host : string; port : int; debug : bool }
809+810+ let server_codec = Init.Section.(
811+ obj (fun host port debug -> { host; port; debug })
812+ |> mem "host" Init.string ~enc:(fun s -> s.host)
813+ |> mem "port" Init.int ~enc:(fun s -> s.port)
814+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun s -> s.debug)
815+ |> finish
816+ )
817+ ]}
818+819+ The [obj] function takes a constructor. Each [mem] call adds an option
820+ and supplies one argument to the constructor. The [~enc] parameter
821+ extracts the value for encoding.
822+823+ {2:section_optional Optional and Absent Values}
824+825+ {ul
826+ {- [mem] with [~dec_absent:v] uses [v] if the option is missing.}
827+ {- [opt_mem] decodes to [None] if the option is missing.}
828+ {- Both still require the option name for encoding.}} *)
829module Section : sig
830831 type 'a codec = 'a t
832+ (** Alias for the codec type. *)
833834 type ('o, 'dec) map
835+ (** The type for section maps under construction. ['o] is the final
836+ OCaml type being built. ['dec] is the remaining constructor
837+ arguments needed. *)
838839 val obj : ?kind:string -> ?doc:string -> 'dec -> ('o, 'dec) map
840+ (** [obj f] starts building a section codec with constructor [f].
841+842+ The constructor [f] should have type
843+ [arg1 -> arg2 -> ... -> argN -> 'o] where each argument corresponds
844+ to a {!mem} or {!opt_mem} call. *)
845846 val mem : ?doc:string -> ?dec_absent:'a -> ?enc:('o -> 'a) ->
847 ?enc_omit:('a -> bool) ->
848 string -> 'a codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
849+ (** [mem name c m] adds option [name] to the section map.
850+851+ {ul
852+ {- [name] is the option name (case-insensitive for lookup).}
853+ {- [c] is the codec for the option's value.}
854+ {- [~dec_absent] provides a default if the option is missing.
855+ Without this, a missing option is an error.}
856+ {- [~enc] extracts the value from the record for encoding.
857+ Required for encoding.}
858+ {- [~enc_omit] if [true] for a value, omits the option during
859+ encoding.}} *)
860861 val opt_mem : ?doc:string -> ?enc:('o -> 'a option) ->
862 string -> 'a codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
863+ (** [opt_mem name c m] adds an optional option. Decodes to [None] if
864+ the option is absent, [Some v] otherwise.
865+866+ {b Example:}
867+ {@ocaml[
868+ |> opt_mem "timeout" Init.int ~enc:(fun s -> s.timeout)
869+ (* Absent -> None, "30" -> Some 30 *)
870+ ]} *)
871+872+ (** {2:unknown Handling Unknown Options}
873+874+ By default, unknown options in a section are ignored ({!skip_unknown}).
875+ You can change this behavior. *)
876877 val skip_unknown : ('o, 'dec) map -> ('o, 'dec) map
878+ (** [skip_unknown m] ignores options not declared with {!mem}.
879+ This is the default behavior, matching Python's configparser. *)
880881 val error_unknown : ('o, 'dec) map -> ('o, 'dec) map
882+ (** [error_unknown m] raises an error if the section contains options
883+ not declared with {!mem}. Use this for strict validation. *)
884885 val keep_unknown : ?enc:('o -> (string * string) list) ->
886 ('o, (string * string) list -> 'dec) map -> ('o, 'dec) map
887+ (** [keep_unknown m] captures unknown options as a list of
888+ [(name, value)] pairs. Useful for pass-through or dynamic configs.
889+890+ {b Example:}
891+ {@ocaml[
892+ type section = {
893+ known : string;
894+ extra : (string * string) list;
895+ }
896+897+ let codec = Init.Section.(
898+ obj (fun known extra -> { known; extra })
899+ |> mem "known" Init.string ~enc:(fun s -> s.known)
900+ |> keep_unknown ~enc:(fun s -> s.extra)
901+ |> finish
902+ )
903+ ]} *)
904905 val finish : ('o, 'o) map -> 'o codec
906+ (** [finish m] completes the section codec. The map's constructor must
907+ be fully saturated (all arguments provided via {!mem} calls). *)
908end
909910+911(** {1:documents Document Codecs}
912913+ Build codecs for complete INI documents. A document codec maps
914+ sections to fields of an OCaml record.
915+916+ {2:document_pattern The Pattern}
917+918+ {@ocaml[
919+ type config = {
920+ server : server;
921+ database : database option;
922+ }
923+924+ let config_codec = Init.Document.(
925+ obj (fun server database -> { server; database })
926+ |> section "server" server_codec ~enc:(fun c -> c.server)
927+ |> opt_section "database" database_codec ~enc:(fun c -> c.database)
928+ |> finish
929+ )
930+ ]} *)
931module Document : sig
932933 type 'a codec = 'a t
934+ (** Alias for the codec type. *)
935936 type ('o, 'dec) map
937+ (** The type for document maps under construction. *)
938939 val obj : ?kind:string -> ?doc:string -> 'dec -> ('o, 'dec) map
940 (** [obj f] starts building a document codec with constructor [f]. *)
941942 val section : ?doc:string -> ?enc:('o -> 'a) ->
943 string -> 'a Section.codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
944+ (** [section name c m] adds a required section.
945+946+ The section must be present in the INI file. If it's missing, decoding
947+ fails with {!Error.Missing_section}.
948+949+ {b Note:} Section names are case-sensitive. [[Server]] and [[server]]
950+ are different sections. *)
951952 val opt_section : ?doc:string -> ?enc:('o -> 'a option) ->
953 string -> 'a Section.codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
954+ (** [opt_section name c m] adds an optional section.
955+956+ Decodes to [None] if the section is absent, [Some v] otherwise. *)
957958 val defaults : ?doc:string -> ?enc:('o -> 'a) ->
959 'a Section.codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
960+ (** [defaults c m] decodes the [[DEFAULT]] section.
961+962+ The DEFAULT section's values are available for interpolation in all
963+ other sections. Use this when you need to access DEFAULT values
964+ directly in your OCaml type.
965+966+ {b Note:} Even without this, DEFAULT values are used for interpolation
967+ and as fallbacks during option lookup. *)
968969 val opt_defaults : ?doc:string -> ?enc:('o -> 'a option) ->
970 'a Section.codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
971+ (** [opt_defaults c m] optionally decodes the [[DEFAULT]] section. *)
972+973+ (** {2:unknown_sections Handling Unknown Sections} *)
974975 val skip_unknown : ('o, 'dec) map -> ('o, 'dec) map
976+ (** [skip_unknown m] ignores sections not declared with {!section}.
977+ This is the default behavior. *)
978979 val error_unknown : ('o, 'dec) map -> ('o, 'dec) map
980+ (** [error_unknown m] raises an error if the document contains sections
981+ not declared with {!section}. Use for strict validation. *)
982983 val finish : ('o, 'o) map -> 'o codec
984 (** [finish m] completes the document codec. *)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** Init Cookbook
7+8+ This file contains complete, runnable examples demonstrating common
9+ patterns for using the Init library. Each example is self-contained
10+ and can be adapted to your use case.
11+12+ Run with: [dune exec ./test/cookbook.exe] *)
13+14+(** {1 Basic Configuration}
15+16+ The simplest use case: parse a configuration file with a single section
17+ into an OCaml record. *)
18+19+module Basic = struct
20+ type server_config = {
21+ host : string;
22+ port : int;
23+ debug : bool;
24+ }
25+26+ let server_codec = Init.Section.(
27+ obj (fun host port debug -> { host; port; debug })
28+ |> mem "host" Init.string ~enc:(fun c -> c.host)
29+ |> mem "port" Init.int ~enc:(fun c -> c.port)
30+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug)
31+ |> finish
32+ )
33+34+ let config_codec = Init.Document.(
35+ obj (fun server -> server)
36+ |> section "server" server_codec ~enc:Fun.id
37+ |> finish
38+ )
39+40+ let example () =
41+ let ini = {|
42+[server]
43+host = localhost
44+port = 8080
45+debug = yes
46+|} in
47+ match Init_bytesrw.decode_string config_codec ini with
48+ | Ok config ->
49+ Printf.printf "Server: %s:%d (debug=%b)\n"
50+ config.host config.port config.debug
51+ | Error msg ->
52+ Printf.printf "Error: %s\n" msg
53+end
54+55+(** {1 Optional Values and Defaults}
56+57+ Handle missing options gracefully with defaults or optional fields. *)
58+59+module Optional_values = struct
60+ type database_config = {
61+ host : string;
62+ port : int; (* Uses default if missing *)
63+ username : string;
64+ password : string option; (* Optional field *)
65+ ssl : bool;
66+ }
67+68+ let database_codec = Init.Section.(
69+ obj (fun host port username password ssl ->
70+ { host; port; username; password; ssl })
71+ |> mem "host" Init.string ~enc:(fun c -> c.host)
72+ (* dec_absent provides a default value when the option is missing *)
73+ |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun c -> c.port)
74+ |> mem "username" Init.string ~enc:(fun c -> c.username)
75+ (* opt_mem decodes to None when the option is missing *)
76+ |> opt_mem "password" Init.string ~enc:(fun c -> c.password)
77+ |> mem "ssl" Init.bool ~dec_absent:true ~enc:(fun c -> c.ssl)
78+ |> finish
79+ )
80+81+ let config_codec = Init.Document.(
82+ obj Fun.id
83+ |> section "database" database_codec ~enc:Fun.id
84+ |> finish
85+ )
86+87+ let example () =
88+ (* Minimal config - uses defaults *)
89+ let ini = {|
90+[database]
91+host = db.example.com
92+username = admin
93+|} in
94+ match Init_bytesrw.decode_string config_codec ini with
95+ | Ok config ->
96+ Printf.printf "DB: %s@%s:%d (ssl=%b, password=%s)\n"
97+ config.username config.host config.port config.ssl
98+ (match config.password with Some _ -> "***" | None -> "none")
99+ | Error msg ->
100+ Printf.printf "Error: %s\n" msg
101+end
102+103+(** {1 Multiple Sections}
104+105+ Parse a configuration with multiple sections, some required and some
106+ optional. *)
107+108+module Multiple_sections = struct
109+ type server = { host : string; port : int }
110+ type database = { connection : string; pool_size : int }
111+ type cache = { enabled : bool; ttl : int }
112+ type config = {
113+ server : server;
114+ database : database;
115+ cache : cache option; (* Optional section *)
116+ }
117+118+ let server_codec = Init.Section.(
119+ obj (fun host port -> { host; port })
120+ |> mem "host" Init.string ~enc:(fun s -> s.host)
121+ |> mem "port" Init.int ~enc:(fun s -> s.port)
122+ |> finish
123+ )
124+125+ let database_codec = Init.Section.(
126+ obj (fun connection pool_size -> { connection; pool_size })
127+ |> mem "connection" Init.string ~enc:(fun d -> d.connection)
128+ |> mem "pool_size" Init.int ~dec_absent:10 ~enc:(fun d -> d.pool_size)
129+ |> finish
130+ )
131+132+ let cache_codec = Init.Section.(
133+ obj (fun enabled ttl -> { enabled; ttl })
134+ |> mem "enabled" Init.bool ~enc:(fun c -> c.enabled)
135+ |> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl)
136+ |> finish
137+ )
138+139+ let config_codec = Init.Document.(
140+ obj (fun server database cache -> { server; database; cache })
141+ |> section "server" server_codec ~enc:(fun c -> c.server)
142+ |> section "database" database_codec ~enc:(fun c -> c.database)
143+ (* opt_section allows the section to be absent *)
144+ |> opt_section "cache" cache_codec ~enc:(fun c -> c.cache)
145+ |> finish
146+ )
147+148+ let example () =
149+ let ini = {|
150+[server]
151+host = api.example.com
152+port = 443
153+154+[database]
155+connection = postgres://localhost/mydb
156+157+[cache]
158+enabled = yes
159+ttl = 7200
160+|} in
161+ match Init_bytesrw.decode_string config_codec ini with
162+ | Ok config ->
163+ Printf.printf "Server: %s:%d\n" config.server.host config.server.port;
164+ Printf.printf "Database: %s (pool=%d)\n"
165+ config.database.connection config.database.pool_size;
166+ (match config.cache with
167+ | Some c -> Printf.printf "Cache: enabled=%b ttl=%d\n" c.enabled c.ttl
168+ | None -> Printf.printf "Cache: disabled\n")
169+ | Error msg ->
170+ Printf.printf "Error: %s\n" msg
171+end
172+173+(** {1 Lists and Comma-Separated Values}
174+175+ Parse comma-separated lists of values. *)
176+177+module Lists = struct
178+ type config = {
179+ hosts : string list;
180+ ports : int list;
181+ tags : string list;
182+ }
183+184+ let section_codec = Init.Section.(
185+ obj (fun hosts ports tags -> { hosts; ports; tags })
186+ |> mem "hosts" (Init.list Init.string) ~enc:(fun c -> c.hosts)
187+ |> mem "ports" (Init.list Init.int) ~enc:(fun c -> c.ports)
188+ |> mem "tags" (Init.list Init.string) ~dec_absent:[] ~enc:(fun c -> c.tags)
189+ |> finish
190+ )
191+192+ let config_codec = Init.Document.(
193+ obj Fun.id
194+ |> section "cluster" section_codec ~enc:Fun.id
195+ |> finish
196+ )
197+198+ let example () =
199+ let ini = {|
200+[cluster]
201+hosts = node1.example.com, node2.example.com, node3.example.com
202+ports = 8080, 8081, 8082
203+|} in
204+ match Init_bytesrw.decode_string config_codec ini with
205+ | Ok config ->
206+ Printf.printf "Hosts: %s\n" (String.concat ", " config.hosts);
207+ Printf.printf "Ports: %s\n"
208+ (String.concat ", " (List.map string_of_int config.ports));
209+ Printf.printf "Tags: %s\n"
210+ (if config.tags = [] then "(none)" else String.concat ", " config.tags)
211+ | Error msg ->
212+ Printf.printf "Error: %s\n" msg
213+end
214+215+(** {1 Enums and Custom Types}
216+217+ Parse enumerated values and custom types. *)
218+219+module Enums = struct
220+ type log_level = Debug | Info | Warn | Error
221+ type environment = Development | Staging | Production
222+223+ type config = {
224+ log_level : log_level;
225+ environment : environment;
226+ max_connections : int;
227+ }
228+229+ let log_level_codec = Init.enum [
230+ "debug", Debug;
231+ "info", Info;
232+ "warn", Warn;
233+ "error", Error;
234+ ]
235+236+ let environment_codec = Init.enum [
237+ "development", Development;
238+ "dev", Development; (* Alias *)
239+ "staging", Staging;
240+ "production", Production;
241+ "prod", Production; (* Alias *)
242+ ]
243+244+ let section_codec = Init.Section.(
245+ obj (fun log_level environment max_connections ->
246+ { log_level; environment; max_connections })
247+ |> mem "log_level" log_level_codec ~dec_absent:Info
248+ ~enc:(fun c -> c.log_level)
249+ |> mem "environment" environment_codec ~enc:(fun c -> c.environment)
250+ |> mem "max_connections" Init.int ~dec_absent:100
251+ ~enc:(fun c -> c.max_connections)
252+ |> finish
253+ )
254+255+ let config_codec = Init.Document.(
256+ obj Fun.id
257+ |> section "app" section_codec ~enc:Fun.id
258+ |> finish
259+ )
260+261+ let example () =
262+ let ini = {|
263+[app]
264+log_level = debug
265+environment = prod
266+|} in
267+ match Init_bytesrw.decode_string config_codec ini with
268+ | Ok config ->
269+ let env_str = match config.environment with
270+ | Development -> "development"
271+ | Staging -> "staging"
272+ | Production -> "production"
273+ in
274+ Printf.printf "Env: %s, Log: %s, MaxConn: %d\n"
275+ env_str
276+ (match config.log_level with
277+ | Debug -> "debug" | Info -> "info"
278+ | Warn -> "warn" | Error -> "error")
279+ config.max_connections
280+ | Error msg ->
281+ Printf.printf "Error: %s\n" msg
282+end
283+284+(** {1 Handling Unknown Options}
285+286+ Three strategies for dealing with options you didn't expect. *)
287+288+module Unknown_options = struct
289+ (* Strategy 1: Skip unknown (default) - silently ignore extra options *)
290+ module Skip = struct
291+ type config = { known_key : string }
292+293+ let section_codec = Init.Section.(
294+ obj (fun known_key -> { known_key })
295+ |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
296+ |> skip_unknown (* This is the default *)
297+ |> finish
298+ )
299+300+ let _config_codec = Init.Document.(
301+ obj Fun.id
302+ |> section "test" section_codec ~enc:Fun.id
303+ |> finish
304+ )
305+ end
306+307+ (* Strategy 2: Error on unknown - strict validation *)
308+ module Strict = struct
309+ type config = { known_key : string }
310+311+ let section_codec = Init.Section.(
312+ obj (fun known_key -> { known_key })
313+ |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
314+ |> error_unknown (* Reject unknown options *)
315+ |> finish
316+ )
317+318+ let _config_codec = Init.Document.(
319+ obj Fun.id
320+ |> section "test" section_codec ~enc:Fun.id
321+ |> error_unknown (* Also reject unknown sections *)
322+ |> finish
323+ )
324+ end
325+326+ (* Strategy 3: Keep unknown - capture for pass-through *)
327+ module Passthrough = struct
328+ type config = {
329+ known_key : string;
330+ extra : (string * string) list;
331+ }
332+333+ let section_codec = Init.Section.(
334+ obj (fun known_key extra -> { known_key; extra })
335+ |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
336+ |> keep_unknown ~enc:(fun c -> c.extra)
337+ |> finish
338+ )
339+340+ let config_codec = Init.Document.(
341+ obj Fun.id
342+ |> section "test" section_codec ~enc:Fun.id
343+ |> finish
344+ )
345+346+ let example () =
347+ let ini = {|
348+[test]
349+known_key = hello
350+extra1 = world
351+extra2 = foo
352+|} in
353+ match Init_bytesrw.decode_string config_codec ini with
354+ | Ok config ->
355+ Printf.printf "Known: %s\n" config.known_key;
356+ List.iter (fun (k, v) ->
357+ Printf.printf "Extra: %s = %s\n" k v
358+ ) config.extra
359+ | Error msg ->
360+ Printf.printf "Error: %s\n" msg
361+ end
362+end
363+364+(** {1 Interpolation}
365+366+ Variable substitution in values. *)
367+368+module Interpolation = struct
369+ (* Basic interpolation: %(name)s *)
370+ module Basic = struct
371+ type paths = {
372+ base : string;
373+ data : string;
374+ logs : string;
375+ config : string;
376+ }
377+378+ let paths_codec = Init.Section.(
379+ obj (fun base data logs config -> { base; data; logs; config })
380+ |> mem "base" Init.string ~enc:(fun p -> p.base)
381+ |> mem "data" Init.string ~enc:(fun p -> p.data)
382+ |> mem "logs" Init.string ~enc:(fun p -> p.logs)
383+ |> mem "config" Init.string ~enc:(fun p -> p.config)
384+ |> finish
385+ )
386+387+ let config_codec = Init.Document.(
388+ obj Fun.id
389+ |> section "paths" paths_codec ~enc:Fun.id
390+ |> finish
391+ )
392+393+ let example () =
394+ let ini = {|
395+[paths]
396+base = /opt/myapp
397+data = %(base)s/data
398+logs = %(base)s/logs
399+config = %(base)s/etc
400+|} in
401+ match Init_bytesrw.decode_string config_codec ini with
402+ | Ok paths ->
403+ Printf.printf "Base: %s\n" paths.base;
404+ Printf.printf "Data: %s\n" paths.data;
405+ Printf.printf "Logs: %s\n" paths.logs;
406+ Printf.printf "Config: %s\n" paths.config
407+ | Error msg ->
408+ Printf.printf "Error: %s\n" msg
409+ end
410+411+ (* Extended interpolation: ${section:name} *)
412+ module Extended = struct
413+ type common = { base : string }
414+ type server = { data_dir : string; log_dir : string }
415+ type config = { common : common; server : server }
416+417+ let common_codec = Init.Section.(
418+ obj (fun base -> { base })
419+ |> mem "base" Init.string ~enc:(fun c -> c.base)
420+ |> finish
421+ )
422+423+ let server_codec = Init.Section.(
424+ obj (fun data_dir log_dir -> { data_dir; log_dir })
425+ |> mem "data_dir" Init.string ~enc:(fun s -> s.data_dir)
426+ |> mem "log_dir" Init.string ~enc:(fun s -> s.log_dir)
427+ |> finish
428+ )
429+430+ let config_codec = Init.Document.(
431+ obj (fun common server -> { common; server })
432+ |> section "common" common_codec ~enc:(fun c -> c.common)
433+ |> section "server" server_codec ~enc:(fun c -> c.server)
434+ |> finish
435+ )
436+437+ let example () =
438+ let config = { Init_bytesrw.default_config with
439+ interpolation = `Extended_interpolation } in
440+ let ini = {|
441+[common]
442+base = /opt/myapp
443+444+[server]
445+data_dir = ${common:base}/data
446+log_dir = ${common:base}/logs
447+|} in
448+ match Init_bytesrw.decode_string ~config config_codec ini with
449+ | Ok cfg ->
450+ Printf.printf "Base: %s\n" cfg.common.base;
451+ Printf.printf "Data: %s\n" cfg.server.data_dir;
452+ Printf.printf "Log: %s\n" cfg.server.log_dir
453+ | Error msg ->
454+ Printf.printf "Error: %s\n" msg
455+ end
456+end
457+458+(** {1 The DEFAULT Section}
459+460+ The DEFAULT section provides fallback values for all other sections. *)
461+462+module Defaults = struct
463+ type section = {
464+ host : string;
465+ port : int;
466+ timeout : int; (* Falls back to DEFAULT *)
467+ }
468+469+ type config = {
470+ production : section;
471+ staging : section;
472+ }
473+474+ let section_codec = Init.Section.(
475+ obj (fun host port timeout -> { host; port; timeout })
476+ |> mem "host" Init.string ~enc:(fun s -> s.host)
477+ |> mem "port" Init.int ~enc:(fun s -> s.port)
478+ |> mem "timeout" Init.int ~enc:(fun s -> s.timeout)
479+ |> finish
480+ )
481+482+ let config_codec = Init.Document.(
483+ obj (fun production staging -> { production; staging })
484+ |> section "production" section_codec ~enc:(fun c -> c.production)
485+ |> section "staging" section_codec ~enc:(fun c -> c.staging)
486+ |> skip_unknown (* Ignore DEFAULT section in output *)
487+ |> finish
488+ )
489+490+ let example () =
491+ let ini = {|
492+[DEFAULT]
493+timeout = 30
494+495+[production]
496+host = api.example.com
497+port = 443
498+499+[staging]
500+host = staging.example.com
501+port = 8443
502+timeout = 60
503+|} in
504+ match Init_bytesrw.decode_string config_codec ini with
505+ | Ok config ->
506+ Printf.printf "Production: %s:%d (timeout=%d)\n"
507+ config.production.host config.production.port config.production.timeout;
508+ Printf.printf "Staging: %s:%d (timeout=%d)\n"
509+ config.staging.host config.staging.port config.staging.timeout
510+ | Error msg ->
511+ Printf.printf "Error: %s\n" msg
512+end
513+514+(** {1 Round-Trip Encoding}
515+516+ Decode, modify, and re-encode a configuration. *)
517+518+module Roundtrip = struct
519+ type config = {
520+ host : string;
521+ port : int;
522+ }
523+524+ let section_codec = Init.Section.(
525+ obj (fun host port -> { host; port })
526+ |> mem "host" Init.string ~enc:(fun c -> c.host)
527+ |> mem "port" Init.int ~enc:(fun c -> c.port)
528+ |> finish
529+ )
530+531+ let config_codec = Init.Document.(
532+ obj Fun.id
533+ |> section "server" section_codec ~enc:Fun.id
534+ |> finish
535+ )
536+537+ let example () =
538+ (* Decode *)
539+ let ini = {|
540+[server]
541+host = localhost
542+port = 8080
543+|} in
544+ match Init_bytesrw.decode_string config_codec ini with
545+ | Error msg ->
546+ Printf.printf "Decode error: %s\n" msg
547+ | Ok config ->
548+ Printf.printf "Decoded: %s:%d\n" config.host config.port;
549+550+ (* Modify *)
551+ let modified = { config with port = 9000 } in
552+553+ (* Encode *)
554+ (match Init_bytesrw.encode_string config_codec modified with
555+ | Ok output ->
556+ Printf.printf "Encoded:\n%s" output
557+ | Error msg ->
558+ Printf.printf "Encode error: %s\n" msg)
559+end
560+561+(** {1 Custom Boolean Formats}
562+563+ Different applications use different boolean representations. *)
564+565+module Custom_booleans = struct
566+ type config = {
567+ python_style : bool; (* 1/yes/true/on or 0/no/false/off *)
568+ strict_01 : bool; (* Only 0 or 1 *)
569+ yes_no : bool; (* Only yes or no *)
570+ on_off : bool; (* Only on or off *)
571+ }
572+573+ let section_codec = Init.Section.(
574+ obj (fun python_style strict_01 yes_no on_off ->
575+ { python_style; strict_01; yes_no; on_off })
576+ |> mem "python_style" Init.bool ~enc:(fun c -> c.python_style)
577+ |> mem "strict_01" Init.bool_01 ~enc:(fun c -> c.strict_01)
578+ |> mem "yes_no" Init.bool_yesno ~enc:(fun c -> c.yes_no)
579+ |> mem "on_off" Init.bool_onoff ~enc:(fun c -> c.on_off)
580+ |> finish
581+ )
582+583+ let config_codec = Init.Document.(
584+ obj Fun.id
585+ |> section "flags" section_codec ~enc:Fun.id
586+ |> finish
587+ )
588+589+ let example () =
590+ let ini = {|
591+[flags]
592+python_style = YES
593+strict_01 = 1
594+yes_no = no
595+on_off = on
596+|} in
597+ match Init_bytesrw.decode_string config_codec ini with
598+ | Ok config ->
599+ Printf.printf "python_style=%b strict_01=%b yes_no=%b on_off=%b\n"
600+ config.python_style config.strict_01 config.yes_no config.on_off
601+ | Error msg ->
602+ Printf.printf "Error: %s\n" msg
603+end
604+605+(** {1 Error Handling}
606+607+ Demonstrate different error scenarios and how to handle them. *)
608+609+module Error_handling = struct
610+ type config = { port : int }
611+612+ let section_codec = Init.Section.(
613+ obj (fun port -> { port })
614+ |> mem "port" Init.int ~enc:(fun c -> c.port)
615+ |> finish
616+ )
617+618+ let config_codec = Init.Document.(
619+ obj Fun.id
620+ |> section "server" section_codec ~enc:Fun.id
621+ |> finish
622+ )
623+624+ let example () =
625+ (* Missing section *)
626+ let ini1 = {|
627+[wrong_name]
628+port = 8080
629+|} in
630+ (match Init_bytesrw.decode_string config_codec ini1 with
631+ | Ok _ -> Printf.printf "Unexpected success\n"
632+ | Error msg -> Printf.printf "Missing section: %s\n\n" msg);
633+634+ (* Missing option *)
635+ let ini2 = {|
636+[server]
637+host = localhost
638+|} in
639+ (match Init_bytesrw.decode_string config_codec ini2 with
640+ | Ok _ -> Printf.printf "Unexpected success\n"
641+ | Error msg -> Printf.printf "Missing option: %s\n\n" msg);
642+643+ (* Type mismatch *)
644+ let ini3 = {|
645+[server]
646+port = not_a_number
647+|} in
648+ (match Init_bytesrw.decode_string config_codec ini3 with
649+ | Ok _ -> Printf.printf "Unexpected success\n"
650+ | Error msg -> Printf.printf "Type mismatch: %s\n\n" msg);
651+652+ (* Using structured errors *)
653+ let ini4 = {|
654+[server]
655+port = abc
656+|} in
657+ match Init_bytesrw.decode_string' config_codec ini4 with
658+ | Ok _ -> Printf.printf "Unexpected success\n"
659+ | Error e ->
660+ Printf.printf "Structured error:\n";
661+ Printf.printf " Kind: %s\n" (Init.Error.kind_to_string (Init.Error.kind e));
662+ Format.printf " Path: %a\n" Init.Path.pp (Init.Error.path e)
663+end
664+665+(** {1 Running Examples} *)
666+667+let () =
668+ Printf.printf "=== Basic Configuration ===\n";
669+ Basic.example ();
670+ Printf.printf "\n";
671+672+ Printf.printf "=== Optional Values ===\n";
673+ Optional_values.example ();
674+ Printf.printf "\n";
675+676+ Printf.printf "=== Multiple Sections ===\n";
677+ Multiple_sections.example ();
678+ Printf.printf "\n";
679+680+ Printf.printf "=== Lists ===\n";
681+ Lists.example ();
682+ Printf.printf "\n";
683+684+ Printf.printf "=== Enums ===\n";
685+ Enums.example ();
686+ Printf.printf "\n";
687+688+ Printf.printf "=== Unknown Options (Passthrough) ===\n";
689+ Unknown_options.Passthrough.example ();
690+ Printf.printf "\n";
691+692+ Printf.printf "=== Basic Interpolation ===\n";
693+ Interpolation.Basic.example ();
694+ Printf.printf "\n";
695+696+ Printf.printf "=== Extended Interpolation ===\n";
697+ Interpolation.Extended.example ();
698+ Printf.printf "\n";
699+700+ Printf.printf "=== DEFAULT Section ===\n";
701+ Defaults.example ();
702+ Printf.printf "\n";
703+704+ Printf.printf "=== Round-Trip Encoding ===\n";
705+ Roundtrip.example ();
706+ Printf.printf "\n";
707+708+ Printf.printf "=== Custom Booleans ===\n";
709+ Custom_booleans.example ();
710+ Printf.printf "\n";
711+712+ Printf.printf "=== Error Handling ===\n";
713+ Error_handling.example ()
+40
test/data/basic.ini
···0000000000000000000000000000000000000000
···1+# Basic INI test file - matches Python configparser basic test
2+3+[Foo Bar]
4+foo=bar1
5+6+[Spacey Bar]
7+foo = bar2
8+9+[Spacey Bar From The Beginning]
10+ foo = bar3
11+ baz = qwe
12+13+[Commented Bar]
14+foo: bar4 ; comment
15+baz=qwe #another one
16+17+[Long Line]
18+foo: this line is much, much longer than my editor
19+ likes it.
20+21+[Section\with$weird%characters[ ]
22+23+[Internationalized Stuff]
24+foo[bg]: Bulgarian
25+foo=Default
26+foo[en]=English
27+foo[de]=Deutsch
28+29+[Spaces]
30+key with spaces : value
31+another with spaces = splat!
32+33+[Types]
34+int : 42
35+float = 0.44
36+boolean = NO
37+123 : strange but acceptable
38+39+[This One Has A ] In It]
40+ forks = spoons
+17
test/data/booleans.ini
···00000000000000000
···1+# Boolean test cases - all valid Python configparser boolean values
2+[BOOLTEST]
3+T1=1
4+T2=TRUE
5+T3=True
6+T4=oN
7+T5=yes
8+F1=0
9+F2=FALSE
10+F3=False
11+F4=oFF
12+F5=nO
13+E1=2
14+E2=foo
15+E3=-1
16+E4=0.1
17+E5=FALSE AND MORE
+12
test/data/case_sensitivity.ini
···000000000000
···1+# Case sensitivity tests
2+# Section names are case-sensitive
3+# Option names are case-insensitive
4+5+[Section]
6+OPTION = value1
7+8+[SECTION]
9+option = value2
10+11+[section]
12+Option = value3
+20
test/data/comments.ini
···00000000000000000000
···1+# This is a comment
2+; This is also a comment
3+4+[section]
5+# Comment before option
6+key1 = value1
7+key2 = value2 ; inline comment
8+key3 = value3 # another inline comment
9+10+; Comment between options
11+12+key4 = value4
13+14+[Commented Bar]
15+baz=qwe ; a comment
16+foo: bar # not a comment!
17+# but this is a comment
18+; another comment
19+quirk: this;is not a comment
20+; a space must precede an inline comment
···1+# Basic interpolation test cases
2+[Foo]
3+bar=something %(with1)s interpolation (1 step)
4+bar9=something %(with9)s lots of interpolation (9 steps)
5+bar10=something %(with10)s lots of interpolation (10 steps)
6+bar11=something %(with11)s lots of interpolation (11 steps)
7+with11=%(with10)s
8+with10=%(with9)s
9+with9=%(with8)s
10+with8=%(With7)s
11+with7=%(WITH6)s
12+with6=%(with5)s
13+With5=%(with4)s
14+WITH4=%(with3)s
15+with3=%(with2)s
16+with2=%(with1)s
17+with1=with
18+19+[Mutual Recursion]
20+foo=%(bar)s
21+bar=%(foo)s
22+23+[Interpolation Error]
24+# no definition for 'reference'
25+name=%(reference)s
+22
test/data/interpolation_extended.ini
···0000000000000000000000
···1+# Extended interpolation test cases
2+[common]
3+favourite Beatle = Paul
4+favourite color = green
5+6+[tom]
7+favourite band = ${favourite color} day
8+favourite pope = John ${favourite Beatle} II
9+sequel = ${favourite pope}I
10+11+[ambv]
12+favourite Beatle = George
13+son of Edward VII = ${favourite Beatle} V
14+son of George V = ${son of Edward VII}I
15+16+[stanley]
17+favourite Beatle = ${ambv:favourite Beatle}
18+favourite pope = ${tom:favourite pope}
19+favourite color = black
20+favourite state of mind = paranoid
21+favourite movie = soylent ${common:favourite color}
22+favourite song = ${favourite color} sabbath - ${favourite state of mind}
+24
test/data/multiline.ini
···000000000000000000000000
···1+# Multiline value test cases
2+[Long Line]
3+foo: this line is much, much longer than my editor
4+ likes it.
5+6+[DEFAULT]
7+foo: another very
8+ long line
9+10+[Long Line - With Comments!]
11+test : we ; can
12+ also ; place
13+ comments ; in
14+ multiline ; values
15+16+[MultiValue]
17+value1 = first line
18+ second line
19+ third line
20+21+value2 = line 1
22+ line 2
23+ line 3
24+ line 4