INI parser and encoder using bytesrw.
This module provides functions to parse and encode INI files using the bytesrw streaming I/O library. It implements Python's configparser semantics for maximum compatibility.
Basic Usage
(* Define your configuration type and codec *)
let config_codec = Init.Document.(
obj (fun server -> server)
|> section "server" server_codec ~enc:Fun.id
|> finish
)
(* Decode from a string *)
match Init_bytesrw.decode_string config_codec ini_text with
| Ok config -> (* use config *)
| Error msg -> (* handle error *)
(* Encode back to a string *)
match Init_bytesrw.encode_string config_codec config with
| Ok text -> (* write text *)
| Error msg -> (* handle error *)Python Compatibility
This parser implements the same semantics as Python's configparser module. Configuration files that work with Python will work here, and vice versa.
Supported Syntax
# Comments start with # or ;
; This is also a comment
[section]
key = value
key2 : value2 ; Both = and : are delimiters
key3=no spaces needed
[multiline]
long_value = This is a long value
that continues on indented lines
for as long as needed
[types]
integer = 42
float = 3.14
boolean = yes ; Also: true, on, 1, no, false, off, 0
list = a, b, c, dEdge Cases and Gotchas
- Section names are case-sensitive:
[Server]and[server]are different. - Option names are case-insensitive:
Portandportare the same. - Whitespace is trimmed from keys and values automatically.
- Empty values are allowed:
key =gives an empty string. - Comments are NOT preserved during round-trips (matching Python).
- Inline comments are disabled by default:
key = value ; commentgives the value"value ; comment"unless you configureinline_comment_prefixes.
Parser Configuration
Configure the parser to match different INI dialects. The default configuration matches Python's ConfigParser.
type interpolation = [ | `No_interpolation(*No variable substitution. Values like
"%(foo)s"are returned literally. Equivalent to Python'sRawConfigParser.Use this for configuration files that contain literal
*)%or$characters that shouldn't be interpreted.| `Basic_interpolation(*Basic variable substitution using
%(name)ssyntax (default). Equivalent to Python'sConfigParserdefault.Variables reference options in the current section or the DEFAULT section:
[paths] base = /opt/app data = %(base)s/data ; Becomes "/opt/app/data"Escaping: Use
*)%%to get a literal%.| `Extended_interpolation(*Extended substitution using
$\{section:name\}syntax. Equivalent to Python'sExtendedInterpolation.Variables can reference options in any section:
[common] base = /opt/app [server] data = ${common:base}/data ; Cross-section reference logs = ${base}/logs ; Same section or DEFAULTEscaping: Use
*)$$to get a literal$.
]The type for interpolation modes. Controls how variable references in values are expanded.
Recursion limit: Interpolation follows references up to 10 levels deep to prevent infinite loops. Deeper nesting raises an error.
Missing references: If a referenced option doesn't exist, decoding fails with Init.Error.kind.Interpolation.
type config = {delimiters : string list;(*Characters that separate option names from values. Default:
["="; ":"].The first delimiter on a line is used, so values can contain delimiter characters:
*)url = https://example.com:8080 ; Colon in value is finecomment_prefixes : string list;(*Prefixes that start full-line comments. Default:
["#"; ";"].A line starting with any of these (after optional whitespace) is treated as a comment and ignored.
*)inline_comment_prefixes : string list;(*Prefixes that start inline comments. Default:
[](disabled).Warning: Enabling inline comments (e.g.,
[";"]) prevents using those characters in values. For example:url = https://example.com;port=8080 ; Would be truncated!A space must precede inline comments:
*)value;commentkeeps the semicolon, butvalue ; commentremoves it.default_section : string;(*Name of the default section. Default:
"DEFAULT".Options in this section are inherited by all other sections and available for interpolation. You can customize this, e.g., to
*)"general"or"common".interpolation : interpolation;(*How to handle variable references. Default:
`Basic_interpolation.See
*)interpolationfor details on each mode.allow_no_value : bool;(*Allow options without values. Default:
false.When
true, options can appear without a delimiter:[mysqld] skip-innodb ; No = sign, value is None port = 3306Such options decode as
*)Nonewhen usingInit.option.strict : bool;(*Reject duplicate sections and options. Default:
true.When
true, if the same section or option appears twice, decoding fails withInit.Error.kind.Duplicate_sectionorInit.Error.kind.Duplicate_option.When
*)false, later values silently override earlier ones.empty_lines_in_values : bool;(*Allow empty lines in multiline values. Default:
true.When
true, empty lines can be part of multiline values:[section] key = line 1 line 3 ; Empty line 2 is preservedWhen
*)false, empty lines terminate the multiline value.
}Parser configuration. Adjust these settings to parse different INI dialects or to match specific Python configparser settings.
val default_config : configDefault configuration matching Python's configparser.ConfigParser:
delimiters = ["="; ":"]comment_prefixes = ["#"; ";"]inline_comment_prefixes = [](disabled)default_section = "DEFAULT"interpolation = `Basic_interpolationallow_no_value = falsestrict = trueempty_lines_in_values = true
val raw_config : configConfiguration matching Python's configparser.RawConfigParser: same as default_config but with interpolation = `No_interpolation.
Use this when your values contain literal % or $ characters.
Decoding
Parse INI data into OCaml values. All decode functions return Result.t - they never raise exceptions for parse errors.
val decode :
?config:config ->
?locs:bool ->
?layout:bool ->
?file:Init.Textloc.fpath ->
'a Init.t ->
Bytesrw.Bytes.Reader.t ->
('a, string) resultdecode codec r decodes INI data from reader r using codec.
configconfigures the parser. Default:default_config.locsiftrue, preserves source locations in metadata. Default:false.layoutiftrue, preserves whitespace in metadata for layout-preserving round-trips. Default:false.fileis the file path for error messages. Default:"-".
Returns Ok value on success or Error message on failure, where message includes location information when available.
val decode' :
?config:config ->
?locs:bool ->
?layout:bool ->
?file:Init.Textloc.fpath ->
'a Init.t ->
Bytesrw.Bytes.Reader.t ->
('a, Init.Error.t) resultdecode' is like decode but returns a structured error with separate Init.Error.kind, location, and path information.
Use this when you need to programmatically handle different error types or extract location information.
val decode_string :
?config:config ->
?locs:bool ->
?layout:bool ->
?file:Init.Textloc.fpath ->
'a Init.t ->
string ->
('a, string) resultdecode_string codec s decodes INI data from string s.
This is the most common entry point for parsing:
let ini_text = {|
[server]
host = localhost
port = 8080
|} in
Init_bytesrw.decode_string config_codec ini_textval decode_string' :
?config:config ->
?locs:bool ->
?layout:bool ->
?file:Init.Textloc.fpath ->
'a Init.t ->
string ->
('a, Init.Error.t) resultdecode_string' is like decode_string with structured errors.
Encoding
Serialize OCaml values to INI format.
val encode :
?buf:Bytesrw.Bytes.t ->
'a Init.t ->
'a ->
eod:bool ->
Bytesrw.Bytes.Writer.t ->
(unit, string) resultencode codec v ~eod w encodes v to writer w using codec.
bufis an optional scratch buffer for writing.eodiftrue, signals end-of-data after writing.
The output format follows standard INI conventions:
- Sections are written as
[section_name] - Options are written as
key = value - Multiline values are continued with indentation
val encode' :
?buf:Bytesrw.Bytes.t ->
'a Init.t ->
'a ->
eod:bool ->
Bytesrw.Bytes.Writer.t ->
(unit, Init.Error.t) resultencode' is like encode with structured errors.
val encode_string :
?buf:Bytesrw.Bytes.t ->
'a Init.t ->
'a ->
(string, string) resultencode_string codec v encodes v to a string.
let config = { server = { host = "localhost"; port = 8080 } } in
match Init_bytesrw.encode_string config_codec config with
| Ok text -> print_endline text
| Error msg -> failwith msgProduces:
[server]
host = localhost
port = 8080val encode_string' :
?buf:Bytesrw.Bytes.t ->
'a Init.t ->
'a ->
(string, Init.Error.t) resultencode_string' is like encode_string with structured errors.
Layout Preservation
When decoding with ~layout:true, whitespace and comment positions are preserved in the Init.Meta.t values attached to each element. When re-encoding, this information is used to reproduce the original formatting as closely as possible.
Limitations:
- Comments are NOT preserved (matching Python's behavior).
- Whitespace within values may be normalized.
- The output may differ slightly from the input in edge cases.
Performance tip: For maximum performance when you don't need layout preservation, use ~layout:false ~locs:false (the default). Enabling ~locs:true improves error messages at a small cost.
Examples
Simple Configuration
type config = { debug : bool; port : int }
let codec = Init.Document.(
let section = Init.Section.(
obj (fun debug port -> { debug; port })
|> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug)
|> mem "port" Init.int ~dec_absent:8080 ~enc:(fun c -> c.port)
|> finish
) in
obj Fun.id
|> section "server" section ~enc:Fun.id
|> finish
)
let config = Init_bytesrw.decode_string codec "[server]\nport = 9000"
(* Ok { debug = false; port = 9000 } *)Multiple Sections
type db = { host : string; port : int }
type cache = { enabled : bool; ttl : int }
type config = { db : db; cache : cache option }
let db_codec = Init.Section.(
obj (fun host port -> { host; port })
|> mem "host" Init.string ~enc:(fun d -> d.host)
|> mem "port" Init.int ~dec_absent:5432 ~enc:(fun d -> d.port)
|> finish
)
let cache_codec = Init.Section.(
obj (fun enabled ttl -> { enabled; ttl })
|> mem "enabled" Init.bool ~enc:(fun c -> c.enabled)
|> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl)
|> finish
)
let config_codec = Init.Document.(
obj (fun db cache -> { db; cache })
|> section "database" db_codec ~enc:(fun c -> c.db)
|> opt_section "cache" cache_codec ~enc:(fun c -> c.cache)
|> finish
)Interpolation
let paths_codec = Init.Section.(
obj (fun base data logs -> (base, data, logs))
|> mem "base" Init.string ~enc:(fun (b,_,_) -> b)
|> mem "data" Init.string ~enc:(fun (_,d,_) -> d)
|> mem "logs" Init.string ~enc:(fun (_,_,l) -> l)
|> finish
)
let doc_codec = Init.Document.(
obj Fun.id
|> section "paths" paths_codec ~enc:Fun.id
|> finish
)
(* Basic interpolation expands %(base)s *)
let ini = {|
[paths]
base = /opt/app
data = %(base)s/data
logs = %(base)s/logs
|}
match Init_bytesrw.decode_string doc_codec ini with
| Ok (_, data, logs) ->
assert (data = "/opt/app/data");
assert (logs = "/opt/app/logs")
| Error _ -> assert falseDisabling Interpolation
(* Use raw_config for files with literal % characters *)
let config = Init_bytesrw.raw_config
let result = Init_bytesrw.decode_string ~config codec {|
[display]
format = 100%% complete ; Would fail with basic interpolation
|}