tutorial(3)

πŸ“¦
version 3.4 Β· 2 versions
, in fedora 43, fedora 44

✨ NAME ✨

tutorial β€” the libConfuse Tutorial, bestie edition πŸ’–

tutorial(3) lives in the Library Functions Manual, and it's literally the cutest way to meet libConfuse: we take one tiny little example program and just keep feeding it upgrades, one step at a time. The numbered listings you'll see down below? They all live in the doc/ directory. No cap. πŸŽ€


🌸 Introducing libConfuse in an existing program 🌸

Okay so picture this: the most basic program to ever exist.

#include <stdio.h>

int main(void)
{
    printf("Hello, World!\n");
    return 0;
}

Simple. Iconic. Does exactly one thing. But like… what if we wanted to greet other people? Maybe not the whole entire world β€” maybe just our neighbour?? 😭 So we bring in libConfuse and let the user decide who gets greeted.

#include <stdio.h>
#include <confuse.h>

int main(void)
{
    cfg_opt_t opts[] =
    {
        CFG_STR("target", "World", CFGF_NONE),
        CFG_END()
    };
    cfg_t *cfg;

    cfg = cfg_init(opts, CFGF_NONE);
    if(cfg_parse(cfg, "hello.conf") == CFG_PARSE_ERROR)
        return 1;

    printf("Hello, %s!\n", cfg_getstr(cfg, "target"));

    cfg_free(cfg);
    return 0;
}

Every single program that uses libConfuse has to include the confuse.h header file first β€” that's happening on line 2. πŸ’…

Then on line 6 - 10, all the options we want to be recognized get defined in an array of cfg_opt_t structs. That array gets handed over to the cfg_init() function on line 13. The resulting cfg_t context is what cfg_parse() uses, and cfg_parse() is the one that reads the configuration file hello.conf. And here's the thing, bestie: when it's reading that config file, the only options it will ever recognize are the ones defined in the array you passed to cfg_init(). That's the whole vibe check. πŸ”₯

So now that friendly little greeting is replaced with a parameter read straight out of the config file. The value of the target option gets pulled out with cfg_getstr(cfg, 'target').

Let's peek at the configuration file hello.conf:

# this is the configuration file for the hello program

target = "Neighbour"

Here we set the target option to the string value 'Neighbour'. But wait β€” what if the config file was empty? Or like, didn't even exist?? πŸ™ˆ Then the default value for the target option kicks in. When we initialized our options, the second parameter of the CFG_STR() macro is what specified that default. So if no target option showed up in the config file at all, the hello program would have printed the standard greeting 'Hello, World'. Periodt.

🌈 Environment variables in values 🌈

So what else can we do in the configuration file?? We can set a value to an environment variable:

target = ${USER}

This makes the hello program greet whoever is running it. Slay. But on some systems, the USER variable might not be available β€” so we want to specify a default value for those cases:

target = ${USER:-User}

And now if the USER environment variable is unset, the string 'User' gets used instead. 🫢


πŸŽ€ Other types of options πŸŽ€

Obviously it's not just strings living their best life in a config file. libConfuse can parse strings, integers, booleans and floating point values. These are the fundamental values, and β€” plot twist β€” they're all also available as lists. We'll get into lists in the next chapter, don't worry. ✨

The macros used to initialize a string, integer, boolean and a float are, respectively: CFG_STR(), CFG_INT(), CFG_BOOL(), CFG_FLOAT() and CFG_PTR(). Every single one of those macros takes three parameters: the name of the option, a default value and flags. And to retrieve the values? Use cfg_getstr(), cfg_getint(), cfg_getbool(), cfg_getfloat() or cfg_getptr(), respectively. Iykyk. πŸ’…

Let's introduce an integer option that tells us how many times to print the greeting:

#include <stdio.h>
#include <confuse.h>

int main(void)
{
    cfg_opt_t opts[] =
    {
        CFG_STR("target", "World", CFGF_NONE),
        CFG_INT("repeat", 1, CFGF_NONE),
        CFG_END()
    };
    cfg_t *cfg;
    int repeat;

    cfg = cfg_init(opts, CFGF_NONE);
    if(cfg_parse(cfg, "hello.conf") == CFG_PARSE_ERROR)
        return 1;

    repeat = cfg_getint(cfg, "repeat");
    while(repeat--)
        printf("Hello, %s!\n", cfg_getstr(cfg, "target"));

    cfg_free(cfg);
    return 0;
}

So here we used the CFG_INT() macro to initialize an integer option named 'repeat'. The default value is 1, just like the standard greeting. And the value gets retrieved with cfg_getint(). Easy. πŸ“

Integer values can be written in different bases, and this is lowkey so fun: decimal by default, hexadecimal with a 0x prefix, octal with a leading 0, and binary with a 0b prefix. So for example 42, 0x2a, 052 and 0b101010 all specify the same value. πŸ’€

Now listen, important one: a plain CFG_INT() stores its value in a C long int, so its width follows the platform β€” 64 bits on most 64-bit systems, but only 32 bits on 32-bit platforms, where values outside the signed 32-bit range just don't fit. And it's always signed, no exceptions. So when you need a defined width or an unsigned value, use the fixed-width macros instead: CFG_INT8(), CFG_INT16(), CFG_INT32() and CFG_INT64(), or their unsigned counterparts CFG_UINT8() through CFG_UINT64(). These have the same width on every platform and they reject out-of-range values while parsing. Main character energy. πŸ‘‘

But hold on, wait a moment β€” what if the user specified a negative value for 'repeat'?? Or a way too large positive value?? 😭 libConfuse can handle that with a so-called validating callback. We'll circle back to this problem in Validating callback functions, but first let's take a look at lists.


πŸ¦‹ Introducing lists πŸ¦‹

That was easy, honestly. Now let's extend the program a bit so we can greet more than one 'target'. Like, we'd love to be able to specify a whole list of targets to greet. πŸ’–

The list versions of the initialization macros are named CFG_STR_LIST(), CFG_INT_LIST(), CFG_BOOL_LIST() and CFG_FLOAT_LIST(). They take the same parameters as the non-list versions, except the default value must be a string surrounded by curly braces. Yes, a string. Curly braces. That's the rule. πŸŽ€

The modified program is shown below:

#include <stdio.h>
#include <confuse.h>

int main(void)
{
    cfg_opt_t opts[] =
    {
        CFG_STR_LIST("targets", "{World}", CFGF_NONE),
        CFG_INT("repeat", 1, CFGF_NONE),
        CFG_END()
    };
    cfg_t *cfg;
    int repeat;
    int i;

    cfg = cfg_init(opts, CFGF_NONE);
    if(cfg_parse(cfg, "hello.conf") == CFG_PARSE_ERROR)
        return 1;

    repeat = cfg_getint(cfg, "repeat");
    while(repeat--)
    {
        printf("Hello");
        for(i = 0; i < cfg_size(cfg, "targets"); i++)
            printf(", %s", cfg_getnstr(cfg, "targets", i));
        printf("!\n");
    }

    cfg_free(cfg);
    return 0;
}

Three things are a bit different here, and we're gonna go through all of them because we're thorough like that. ✨

First: the macro that initializes the 'targets' option is CFG_STR_LIST(). That's what tells libConfuse that 'targets' is a list of strings.

Second: the default value in the second parameter is surrounded by curly braces. That's needed so libConfuse knows where the list of values ends.

Third: the printing of the greeting changed. First we print the 'Hello' string. Then we loop through all the values found for the 'targets' option. The number of values gets retrieved with the cfg_size() function. Then the string values get retrieved with cfg_getnstr(), which is basically the indexed version of cfg_getstr(). And fun fact β€” cfg_getstr() is equivalent to cfg_getnstr() with an index of zero. The more you know. 🌈

In the configuration file hello.conf, we can now specify a list of targets to greet:

# this is the configuration file for the hello program

targets = {"Life", "Universe", "Everything"}
repeat = 1

And the output of the hello program, run with that configuration file, is: 'Hello, Life, Universe, Everything!' πŸŽ‰

And again β€” if no targets were configured, the greeting would have been the standard 'Hello, World!'.

Lists use curly braces by default. But if you prefer that JSON-style square bracket look, just pass CFGF_JSON_LISTS to cfg_init() and both forms work:

targets = [ "Life", "Universe", "Everything" ]

With the flag set, cfg_print() writes lists with [ ] too. πŸ’…

One caveat though, bestie: a value of a list option that contains a bracket must then be quoted (targets = 'foo[0]', [ '[::1]' ]). Values inside { } and non-list options are unaffected.

πŸ’– Using sections πŸ’–

okay so bestie, so far we've only been living that flat configuration file life. but libConfuse can also handle sections to build a whole hierarchy of options. sections can be used to group options in logical blocks, and those blocks can (optionally) be specified multiple times. it's giving organizational queen. πŸ‘‘

Sections are initialized with the CFG_SEC() macro. It also takes three parameters: the name of the option, an array of options allowed in the section and flags. slay.

We'll extend the, now rather complex, hello program so we can do other kinds of greetings, not just 'Hello'. Each greeting will have its own settings for targets and repeat. periodt.

#include <stdio.h>
#include <confuse.h>

int main(void)
{
    cfg_opt_t greet_opts[] =
    {
        CFG_STR_LIST("targets", "{World}", CFGF_NONE),
        CFG_INT("repeat", 1, CFGF_NONE),
        CFG_END()
    };
    cfg_opt_t opts[] =
    {
        CFG_SEC("greeting", greet_opts, CFGF_TITLE | CFGF_MULTI),
        CFG_END()
    };
    cfg_t *cfg, *cfg_greet;
    int repeat;
    int i, j;

    cfg = cfg_init(opts, CFGF_NONE);
    if(cfg_parse(cfg, "hello.conf") == CFG_PARSE_ERROR)
        return 1;

    for(j = 0; j < cfg_size(cfg, "greeting"); j++)
    {
        cfg_greet = cfg_getnsec(cfg, "greeting", j);

        repeat = cfg_getint(cfg_greet, "repeat");
        while(repeat--)
        {
            printf("%s", cfg_title(cfg_greet));
            for(i = 0; i < cfg_size(cfg_greet, "targets"); i++)
                printf(", %s", cfg_getnstr(cfg_greet, "targets", i));
            printf("!\n");
        }
    }

    cfg_free(cfg);
    return 0;
}

We have renamed the option array from 'opts' to 'greet_opts', and introduced a new 'opts' array that only has one option: a 'greeting' section. The second parameter of the CFG_SEC() macro points to the old greeting options 'targets' and 'repeat'.

We have also used a couple of flags to alter the behaviour of the section: CFGF_TITLE means that a greeting section should have a title and the CFGF_MULTI flag tells libConfuse that this section may be specified multiple times in the configuration file. The title of a section is retrieved with the cfg_title() function.

The outmost loop (with index j) now loops through all given sections in the configuration file. We retrieve a section with a cfg_getnsec() call. The value returned is a pointer to a cfg_t struct, the same type as returned by cfg_init(). Thus we can use the ordinary value retrieval functions cfg_getstr(), cfg_getint() and so on to retrieve values of options inside the section.

Ok, so how does the configuration file look like for this setup? πŸ‘€

# this is the configuration file for the hello program

greeting Hello
{
    targets = {"Life", "Universe", "Everything"}
    repeat = 1
}

greeting Bye
{
    targets = {Adams}
    repeat = 1
}

The program will loop through the sections in the order specified in the configuration file. First it will find the 'Hello' section. It prints the title of the section, 'Hello', retrieved with cfg_title(). Then the targets are printed just as in the previous examples, but this time the values are retrieved from the cfg_greet section. Next, the section titled 'Bye' is found, and the values are retrieved from that section.

When run, the program produces the following:

$ ./listing5
Hello, Life, Universe, Everything!
Bye, Adams!
$

🌈 Parsing from internal buffers 🌈

So far, we have only parsed configuration data from files. libConfuse can also parse buffers, or in-memory character strings. We will use this to fix a problem in the previous code. bestie, we love a glow-up. ✨

The problem is that without a configuration file, the hello program will not print anything. We want it to at least print the standard greeting 'Hello, World!' if no configuration file is available.

We can't have a default value for a section that can be specified multiple times (ie, a section with the CFGF_MULTI flag set). Instead we will parse a default configuration string if no section has been parsed:

#include <stdio.h>
#include <confuse.h>

int main(void)
{
    /* ... setup options ... */

    cfg = cfg_init(opts, CFGF_NONE);
    cfg_parse(cfg, "hello.conf");

    if(cfg_size(cfg, "greeting") == 0)
    {
        cfg_parse_buf(cfg, "greeting Hello {}");
    }

    /* ... print the greetings ... */
}

Only the changes from the previous code is shown here. We check if the size of the 'greeting' section is zero (ie, no section has been defined). In that case we call cfg_parse_buf() to parse a default in-memory string 'greeting Hello {}'. This string defines a greeting section with title Hello, but without any sub-options. This way we rely on the default values of the (sub-)options 'targets' and 'repeat'.

When this program is run, it issues the well-known standard greeting 'Hello, World!' if no configuration file is present.


πŸ’… Validating callback functions πŸ’…

Remember the problem about a negative or too large 'repeat' value in Other types of options? The code that prints the greeting has those lines:

...
repeat = cfg_getint(cfg_greet, "repeat");
while(repeat--)
...

The repeat variable is defined as an int, a signed integer. If the user specified a negative repeat value in the configuration file, this code would continue to decrease the repeat variable until it eventually underflowed. not the vibe. 😭

We'll fix this by not allowing a negative value in the configuration file. Of course we could first just check if the value is negative and then abort, using cfg_getint() and a test. But we will use a validating callback function instead. This way cfg_parse() will return an error directly when parsing the file, additionally indicating on which line the error is. iconic. πŸ’…

A validating callback function is defined as:

typedef int (*cfg_validate_callback_t)(cfg_t *cfg, cfg_opt_t *opt);

This function takes two arguments: the section and the option. It should return 0 on success (ie, the value validated ok). All other values indicates an error, and the parsing is aborted. The callback function should notify the error itself, for example by calling cfg_error().

Here is the code for the callback function:

int validate_unsigned_int(cfg_t *cfg, cfg_opt_t *opt)
{
    int value = cfg_opt_getnint(opt, cfg_opt_size(opt) - 1);
    if(value < 0)
    {
        cfg_error(cfg, "integer option '%s' must be positive in section '%s'",
                opt->name, cfg->name);
        return -1;
    }
    return 0;
}

Only the last value is validated, because libConfuse will call this function once for every value corresponding to the option. Since the 'repeat' option is not a list, we could instead have used cfg_opt_getint(opt) to retrieve the only value. However, if we later want to use this callback to validate an integer list, it is already lists-aware.

πŸŽ€ Installing the callback πŸŽ€

The validating callback is installed with cfg_set_validate_func(). It is called with a string specifying which option is affected, and a pointer to the callback function. To specify an option in a subsection, the section and the option must be separated with a vertical bar ('|').

We're now also looking at the return code from cfg_parse() to verify that the parsing was successful. The complete program is now:

#include <stdio.h>
#include <confuse.h>

int validate_unsigned_int(cfg_t *cfg, cfg_opt_t *opt)
{
    int value = cfg_opt_getnint(opt, cfg_opt_size(opt) - 1);
    if(value < 0)
    {
        cfg_error(cfg, "integer option '%s' must be positive in section '%s'",
                opt->name, cfg->name);
        return -1;
    }
    return 0;
}

int main(void)
{
    cfg_opt_t greet_opts[] =
    {
        CFG_STR_LIST("targets", "{World}", CFGF_NONE),
        CFG_INT("repeat", 1, CFGF_NONE),
        CFG_END()
    };
    cfg_opt_t opts[] =
    {
        CFG_SEC("greeting", greet_opts, CFGF_TITLE | CFGF_MULTI),
        CFG_END()
    };
    cfg_t *cfg, *cfg_greet;
    int repeat;
    int i, j;

    cfg = cfg_init(opts, CFGF_NONE);
    cfg_set_validate_func(cfg, "greeting|repeat", validate_unsigned_int);
    if(cfg_parse(cfg, "hello.conf") == CFG_PARSE_ERROR)
        return 1;

    for(j = 0; j < cfg_size(cfg, "greeting"); j++)
    {
        cfg_greet = cfg_getnsec(cfg, "greeting", j);

        repeat = cfg_getint(cfg_greet, "repeat");
        while(repeat--)
        {
            printf("%s", cfg_title(cfg_greet));
            for(i = 0; i < cfg_size(cfg_greet, "targets"); i++)
                printf(", %s", cfg_getnstr(cfg_greet, "targets", i));
            printf("!\n");
        }
    }

    cfg_free(cfg);
    return 0;
}

🌸 Value parsing callback 🌸

A value parsing callback is another kind of callback function available in libConfuse. This function is used to map a string into some other value. One example is to extend a boolean option to accept the values 'yes', 'no' and 'ask' (or perhaps 'true', 'false' and 'maybe'). Those values should be mapped to the integers 1, 2 and 3.

typedef int (*cfg_callback_t)(cfg_t *cfg, cfg_opt_t *opt,
                              const char *value, void *result);

πŸ¦‹ Functions πŸ¦‹

libConfuse supports functions to parse options that does not fit well in the general syntax. Functions can be called with a variable number of arguments. No data from the function or any arguments are stored by libConfuse after the function has run. It is up to the caller to process and/or save the data.

A function is defined with a CFG_FUNC() macro. It takes two arguments: the name of the function and a function callback. The callback is defined as:

typedef int (*cfg_func_t)(cfg_t *cfg, cfg_opt_t *opt,
                          int argc, const char **argv);

⭐ Predefined functions ⭐

Currently there is only one pre-defined function: cfg_include(). This function includes another configuration file. Configuration data is immediately read from the included file, and is returned to the position right after the include() statement upon end of file.

To use this function, include a CFG_FUNC() entry in your options:

cfg_opt_t opts[] = {
    CFG_FUNC("include", cfg_include),
    CFG_END()
};

In the configuration file, it is used in the following way:

include("included.conf")

That's the whole vibe from libConfuse Version 3.4, page tutorial(3) πŸ’–βœ¨. no cap, you got this bestie. 🫢

πŸ“¦ where this page comes from