Module Bcfg

bcfg is a simple configuration format built on two types: the directive and the list of directives. The format is meant to be small enough to be read by a human, and regular enough to be processed by a program without any ceremony.

A configuration is a value of type t, that is a list of directives.

This module is the level 0 interface: it parses a configuration file into a tree, and writes such a tree back. Nothing more. If you want to map a configuration onto your own OCaml types, have a look at bcfgt, which is built on top of this module.

The format in one example.

The distribution ships an example/ directory with the configuration file used throughout this documentation:

default www.example.org

service www.example.org public {
  memory 128
  listen 80
  listen 443
  upstream {
    kind tcp
    host 10.0.0.2
    port 8080
  }
}

A directive is a name (service), a list of parameters (www.example.org, public) and a list of children, which are directives again. Everything is a string, encoded in UTF-8 (or quoted, when it is not): bcfg has no notion of number, boolean or date, and never guesses one for you. The first directive above parses to:

{ name= "service"; parameters= ["www.example.org"; "public"]; children=
  [{ name= "memory"; parameters= ["128"]; children= [] };
   { name= "listen"; parameters= ["80"]; children= [] };
   ...] }

Process/examine a configuration file.

bcfg provides a parser function that extracts a value of type t from a Lexing.lexbuf source. Here is how to obtain an OCaml value from a configuration file:

let of_filepath filename =
  let ic = open_in_bin filename in
  let finally () = close_in ic in
  Fun.protect ~finally @@ fun () ->
  let lexbuf = Lexing.from_channel ~with_positions:true ic in
  match Bcfg.parser lexbuf with
  | Ok t -> t
  | Error err -> Format.kasprintf failwith "%a" Bcfg.pp_error_for_human err

bcfg is designed to depend on very little cone (it depends only on Menhir and the OCaml runtime). The idea is to be able to use bcfg in more restricted contexts such as unikernels (where the concept of a file may not exist). Reading from a string is just Lexing.from_string.

From there, a configuration is an ordinary OCaml value: extracting information is pattern matching and list manipulation. For instance, collecting the ports on which each service listens:

let ( let* ) = Result.bind
let msgf fmt = Format.kasprintf (fun msg -> `Msg msg) fmt

type service = { hostname : string; listen : int list }

let fields name { Bcfg.children; _ } =
  let fn = function
    | { Bcfg.name = name'; parameters = v :: _; _ } when name = name' ->
        Some v
    | _ -> None
  in
  List.filter_map fn children

let services (t : Bcfg.t) =
  let fn acc directive =
    let* acc = acc in
    match directive with
    | { Bcfg.name = "service"; parameters = hostname :: _; _ } ->
        let ports = fields "listen" directive in
        let none = msgf "%s: invalid port" hostname in
        let* listen =
          List.fold_left
            (fun acc port ->
              let* acc = acc in
              let* port = Option.to_result ~none (int_of_string_opt port) in
              Ok (port :: acc))
            (Ok []) ports
        in
        Ok ({ hostname; listen = List.rev listen } :: acc)
    | _ -> Ok acc
  in
  let* services = List.fold_left fn (Ok []) t in
  Ok (List.rev services)

As you can see, this works, but it is verbose and every codec detail (is the field required? is the number valid?) has to be spelled out by hand. This is precisely what bcfgt does for you, in both directions, from a single description of your type. We recommend it as soon as your configuration has more than a couple of fields.

Error handling.

A configuration file may be malformed. bcfg aims to provide a way of explaining and contextualising lexical and syntactic errors from malformed configuration files.

In this case, our type error can pinpoint the location of the error by associating it with a Txtloc.t. There are two main types of error:

  1. a lexical error, which generally occurs when the document is not in UTF-8
  2. a syntactic error

As regards syntax errors, Menhir suggests contextualising the error in accordance with the syntactic rules set out in bcfg. In this way, it is possible to determine what is missing (such as a closing brace) or what is malformed (such as a word appearing in the wrong place) and to obtain a clear description of the syntax error. With this in mind, we invite you to familiarise yourself with the Error module. The bcfg validate tool is nothing more than this machinery plus Txtloc.lines_around_txtloc to show the offending lines.

Produce a configuration file.

bcfg provides a way to output content in bcfg format from an OCaml value of type t. The aim is to be able to serialise a configuration file. This serialisation may depend on a number of parameters, such as indentation, the way in which a string that is not valid UTF-8 is represented, or the number of columns allowed before a line break (see Out).

let v ?(children = []) name parameters = { Bcfg.name; parameters; children }

let encode filename services =
  let fn { hostname; listen } =
    let children =
      List.map (fun p -> v "listen" [ string_of_int p ]) listen
    in
    v ~children "service" [ hostname ]
  in
  let t = List.map fn services in
  let oc = open_out_bin filename in
  let finally () = close_out oc in
  Fun.protect ~finally @@ fun () ->
  Bcfg.emitter t |> Seq.iter (output_string oc)

Whatever bcfg is able to parse, it must be able to write back without altering it. That property is what the bcfg iso tool checks, and what our fuzzer tries to break.

Large files.

parser builds the whole tree in memory. When the document is too large for that, or when only a few directives are of interest, the Stream module gives a SAX-like view of the same format: a flat sequence of lexemes, or one top-level directive at a time. This is what bcfg query uses when a query does not need to look at the whole document.

type directive = Bcfg_type.directive = {
  1. name : string;
  2. parameters : string list;
  3. children : directive list;
}

A directive: a name, its positional parameters and its children. The directive service www.example.org public { listen 80 } is { name= "service"; parameters= [ "www.example.org"; "public" ]; children= [ { name= "listen"; parameters= [ "80" ]; children= [] } ] }.

The order of the parameters and of the children is the order of the file and is preserved when the value is written back. A name may appear several times among the children: bcfg is not a map, and it is up to the reader to decide whether a repetition is a list or an error.

type t = Bcfg_type.t

A configuration: the list of the top-level directives, in the order in which they appear in the document.

module Txtloc : sig ... end

Txtloc locates the errors which may occur when manipulating a bcfg file. A location alone is rarely useful to a human, so the module also re-reads the source around it and gives back the lines to display, in the spirit of what a compiler prints. This is what bcfg validate uses.

Errors.

A configuration file may be malformed, and bcfg tries hard to explain why rather than to simply refuse it. Errors come in two flavours: the lexer rejects a byte or a word (`Lexer_error), or the parser reads a token which cannot appear where it appears (`Parser_error). Both carry a Txtloc.t, and the syntactic one carries, in addition, the state of the automaton, which is the subject of the Error module below.

module Error : sig ... end

Introspection of syntax errors.

type error = [
  1. | `Lexer_error of Txtloc.t * [ `Invalid_character of char | `Message of string ]
  2. | `Parser_error of Txtloc.t * (Error.state * Error.t) option
  3. | `Rejected
]

Type of errors:

  • `Lexer_error is a byte which cannot start a word (`Invalid_character) or a word which is malformed, such as an unterminated quote or a sequence which is not valid UTF-8 (`Message);
  • `Parser_error is a token which cannot appear where it appears. The second component is None only when the very first token is already wrong, in which case the stack is empty and there is nothing to inspect; see Error for what to do with it otherwise;
  • `Rejected is Menhir's rejection of the input. Since we stop at the first error and our grammar has no error production, it does not happen in practice.
val pp_error_for_human : Format.formatter -> error -> unit

Simple pretty-printer for error values. It says what kind of error occurred, and nothing more: it does not print the location, nor the context. Producing a good message is the job of the caller, with Error and Txtloc, as bcfg validate does.

val parser : Lexing.lexbuf -> (t, [> error ]) result

parser lexbuf parses a whole configuration from lexbuf and returns the list of its top-level directives. The whole document is held in memory; see Stream when this is not acceptable.

Comments and blank lines are not part of the tree: they are consumed by the lexer and do not survive a round-trip through emitter.

module Out : sig ... end
val emitter : ?cfg:Out.cfg -> t -> string Seq.t

emitter ?cfg t renders t as a sequence of chunks, ready to be written to a channel or concatenated. Feeding the result back to parser gives t again, whatever cfg is: the output configuration only affects the layout, never the content.

val pp_as_ocaml_value : Format.formatter -> t -> unit

pp_as_ocaml_value ppf t prints t as an OCaml value (the very shape of directive), which is handy when debugging or when writing a test expectation.

module Stream = Bcfg_stream

Fully streaming, SAX-like API. See Bcfg_stream.