LibUCL is a human-friendly, automation-oriented parser and generator
for Universal Configuration Language
or UCL
. UCL's main benefits
are:
- human readable and editable with comments
- machine parsable
- conversion to/from YAML, JSON
- macro and include-file support
- lightning-fast parser
UCL
is a NIF-based binding to the reference implementation LibUCL,
and currently requires that library to be pre-installed on your system.
It does not use dirty schedulers, nor does it time-slice NIF calls, as this would make the code significantly more complex for a function that is unlikely to be run frequently within the BEAM.
UCL comes with friendly functions for both Elixir and Erlang, we are all friends here.
libucl
is obviously a dependency, and the usual UNIX build chain is
required.
You will need Erlang/OTP 25 or higher, because the build tool enc
is a pre-compiled escript.
- FreeBSD:
textproc/libucl devel/rebar3
- OSX:
brew install libucl
- Debian:
build-essential
from baseerlang-dev erlang-nox
from erlang-solutionsrebar3
from https://rebar3.org/ - OpenBSD:
erlang-25 elixir
from baserebar3
from https://rebar3.org/
On some systems, LibUCL is missing and you'll need to build it. On Debian, there is a conflicting compression library of the same name.
$ doas pkg_add pkgconf autogen autoconf-archive autoconf-2.69 \
automake-1.11 libtool
$ tar xzf libucl-*.gz
$ cd libucl*
$ ./autogen.sh
$ ./configure
$ make
$ doas make install
$ sudo apt install autoconf libcurl4-openssl-dev automake libtool \
autoconf-archive pkg-config
$ tar xzf libucl-*.gz
$ cd libucl*
$ ./autogen.sh
$ ./configure --prefix=/usr
$ make
$ sudo make install
Via git clone:
$ export PATH=$PATH:/usr/local/lib/erlang25/bin:$HOME/.mix
$ mix local.rebar
$ rebar3 do clean, compile, ct
Via hex in the usual fashion:
- Elixir: add
{:ucl, "~> 1.0"}
tomix.exs
- Erlang: add
{ucl, "1.1.0"}
torebar.config
Compilation should work on all UNIX-like OS out of the box. There is
o Windows support planned, but if you can compile libucl
on Windows,
get in touch.
Tests are available as usual via rebar3 ct
, and long-running property
tests via rebar3 proper
. The property tests are effectively fuzzing
the NIF by injecting random binary()
data and expecting it to return
a classic {ok | error, any()}
tuple, and take a considerable amount of
time (several hours):
$ time rebar3 proper
... 3 hours later ...
00% {60000,66000}
10% {54000,60000}
09% {48000,54000}
10% {42000,48000}
10% {36000,42000}
09% {30000,36000}
09% {24000,30000}
09% {18000,24000}
09% {12000,18000}
10% {6000,12000}
9% {0,6000}
===> 1/1 properties passed
OK: Passed 500000 test(s).
11005.99 real 11039.08 user 86.36 sys
Although UCL
is written in Erlang, and uses rebar3
,it should
compile cleanly as a dependency on any BEAM language.
iex> UCL.to_json(":foo")
{:error, :ucl_invalid}
iex> "foo: true" |> UCL.to_json! |> Jason.decode!
%{"foo" => true}
1> ucl:to_json(<<"foo: true">>).
{ok,<<"{\n \"foo\": true\n}">>}
The implementation is intended for functional transformation of config data held in memory, and thus a large portion of the LibUCL API has been summarily ignored. If you need something please let us know.
UCL is heavily infused by nginx
configuration as the example of a
convenient configuration system. However, UCL is fully compatible with
JSON
format and is able to parse json files. For example, you can
write the same configuration in the following ways:
- in nginx like:
param = value;
section {
param = value;
param1 = value1;
flag = true;
number = 10k;
time = 0.2s;
string = "something";
subsection {
host = {
host = "hostname";
port = 900;
}
host = {
host = "hostname";
port = 901;
}
}
}
- or in JSON:
{
"param": "value",
"param1": "value1",
"flag": true,
"subsection": {
"host": [
{
"host": "hostname",
"port": 900
},
{
"host": "hostname",
"port": 901
}
]
}
}
There are various things that make ucl configuration more convenient for editing than strict json:
- Braces are not necessary to enclose a top object: it is automatically treated as an object:
"key": "value"
is equal to:
{"key": "value"}
- There is no requirement of quotes for strings and keys, moreover,
:
may be replaced=
or even be skipped for objects:
key = value;
section {
key = value;
}
is equal to:
{
"key": "value",
"section": {
"key": "value"
}
}
- No commas mess: you can safely place a comma or semicolon for the last element in an array or an object:
{
"key1": "value",
"key2": "value",
}
- Non-unique keys in an object are allowed and are automatically converted to the arrays internally:
{
"key": "value1",
"key": "value2"
}
is converted to:
{
"key": ["value1", "value2"]
}
UCL accepts named keys and organize them into objects hierarchy internally. Here is an example of this process:
section "blah" {
key = value;
}
section foo {
key = value;
}
is converted to the following object:
section {
blah {
key = value;
}
foo {
key = value;
}
}
Plain definitions may be more complex and contain more than a single level of nested objects:
section "blah" "foo" {
key = value;
}
is presented as:
section {
blah {
foo {
key = value;
}
}
}
- Numbers can have suffixes to specify standard multipliers:
[kKmMgG]
- standard 10 base multipliers (so1k
is translated to 1000)[kKmMgG]b
- 2 power multipliers (so1kb
is translated to 1024)[s|min|d|w|y]
- time multipliers, all time values are translated to float number of seconds, for example10min
is translated to 600.0 and10ms
is translated to 0.01
- Hexadecimal integers can be used by
0x
prefix, for examplekey = 0xff
. However, floating point values can use decimal base only. - Booleans can be specified as
true
oryes
oron
andfalse
orno
oroff
. - It is still possible to treat numbers and booleans as strings by enclosing them in double quotes.
UCL supports different style of comments:
- single line:
#
- multiline:
/* ... */
Multiline comments may be nested:
# Sample single line comment
/*
some comment
/* nested comment */
end of comment
*/
- Paul Davis, whose well documented NIF examples made writing this almost cake