Ok, so you’ve written this wonderful tool, and it uses a configuration file. Because you didn’t re-invent the wheel, you used libconfig. But because you need to access the configuration data quickly, you want it in a struct or other native C data structures rather than having to lookup the settings through libconfig’s interface.

That’s the first thing conf2struct will do for you. Suppose your configuration file looks something like the example given in libconfig’s documentation:

version = "1.0";

application:
{
  window:
  {
    title = "My Application";
    size = { w = 640; h = 480; };
    pos = { x = 350; y = 250; };
  };

  list = ( ( "abc", 123, true ), 1.234, ( /* an empty list */ ) );

  books = ( { title  = "Treasure Island";
              author = "Robert Louis Stevenson";
              price  = 29.95;
              qty    = 5; },
            { title  = "Snow Crash";
              author = "Neal Stephenson";
              price  = 9.99;
              qty    = 8; } );

  misc:
  {
    pi = 3.141592654;
    bigint = 9223372036854775807L;
    columns = [ "Last Name", "First Name", "MI" ];
    bitmask = 0x1FC3;	// hex
    umask = 0027;	// octal. Range limited to that of "int"
  };
};

Instead of defining your struct and writing reams of code that reads the configuration into it, simply write a description of your configuration file and give it to conf2struct. Because conf2struct didn’t re-invent the wheel, this is done using libconfig as well. So here we describe all the data that’s in our configuration file:

header: "example.h";
parser: "example.c";

config: {
name: "eg";
items: (
        { name: "version"; type: "string"; },
        { name: "application"; type: "group", items: (
                 { name: "window"; type: "group", items: (
                        { name: "title"; type: "string"; },
                        { name: "size" ; type: "group"; items: (
                             { name: "w"; type: "int" },
                             { name: "h"; type: "int" }
                        ) },
                        { name: "pos" ; type: "group"; items: (
                             { name: "x"; type: "int" },
                             { name: "y"; type: "int" }
                        ) }
                 ) },
                 { name: "books"; type: "list"; items: (
                      { name: "title"; type: "string" },
                      { name: "author"; type: "string" },
                      { name: "price"; type: "float" },
                      { name: "qty"; type: "int" }
                 ) },

                 { name: "misc"; type: "group"; items: (
                      { name: "pi"; type: "float" },
                      { name: "bigint"; type: "int64" },
                      { name: "columns"; type: "array"; element_type: "string" },
                      { name: "bitmask"; type: "int" },
                      { name: "umask"; type: "int" }
                 ) }
        ) }
    )
}

This is pretty straigh-forward: we have a config element that contains a name and a list of items which describe what’s in the root of configuration file. Each item is described as a name and type. The two special types list and group contain a further list of items, which themselves describe what’s in the list.

Here we have a limitation: we do not support lists of items that have no fixed type. We hope that’s not too bad in the real world.

Two additional settings, header and parser, specify the output files into which conf2struct will write the type definitions and the parser code, respectively.

So we end up with a example.h which defines types for each level of the configuration. Each type is prefixed with the name set in the config setting, in our case eg:

/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
 * on Wed Jan 16 22:27:52 2019. */
 
#ifndef C2S_EG_H
#define C2S_EG_H
#include <libconfig.h>


struct eg_application_window_size_item {
	int	w;
	int	h;
};

struct eg_application_window_pos_item {
	int	x;
	int	y;
};

struct eg_application_window_item {
	const char*	title;
	struct eg_application_window_size_item* size;
	struct eg_application_window_pos_item* pos;
};

struct eg_application_books_item {
	const char*	title;
	const char*	author;
	double	price;
	int	qty;
};

struct eg_application_misc_item {
	double	pi;
	long long	bigint;
	size_t	columns_len;
	const char** columns;
	int	bitmask;
	int	umask;
};

struct eg_application_item {
	struct eg_application_window_item* window;
	size_t	books_len;
	struct eg_application_books_item* books;
	struct eg_application_misc_item* misc;
};

struct eg_item {
	const char*	version;
	struct eg_application_item* application;
};

int eg_parse_file(
        const char* filename,
        struct eg_item* eg, 
        const char** errmsg);

void eg_print(
    struct eg_item *eg,
    int depth);

int eg_cl_parse(
    int argc,
    char* argv[],
    struct eg_item *eg);

#endif

We also get the prototypes to 3 functions: a file parser, a pretty-printer, and a command-line parser (more on that later). Now, reading a configuration file is as simple as calling eg_parse_file() on a file, and you’ll get a structure all filled up, with variable-length items allocated and all.

Now wouldn’t it be nice if we had a command-line as well, which read in the same struct, and in fact allowed us to override the settings in the configuration file?

That’s what you get for free with eg_cl_parse(). Because eg_cl_parse() is based on argtable3, it also pretty-prints the options and allows you to define descriptions for each options, which you’ll add in the conf2struct configuration file. There is also a conffile_option which allows to tell conf2struct which option should be defined to read the configuration file. So now we have:

header: "example.h";
parser: "example.c";

conffile_option: ( "F", "conffile" );

config: {
name: "eg";
items: (
        { name: "version"; type: "string"; description: "Specify version number" },
        { name: "application"; type: "group", items: (
                 { name: "window"; type: "group", items: (
[...]
                 ) }
        ) }
    )
}

And now running the application with no command-line parameters will print a nice usage message:


$ ./example -?
eg: invalid option "-?"
 [-F <file>] [--version=<str>] [--application-window-title=<str>]... [--application-window-size-w=<n>]... [--application-window-size-h=<n>]... [--application-window-pos-x=<n>]... [--application-window-pos-y=<n>]... [--application-books-title=<str>]... [--application-books-author=<str>]... [--application-books-price=<n>]... [--application-books-qty=<n>]... [--application-misc-pi=<n>]... [--application-misc-bigint=<n>]... [--application-misc-columns=<str>]... [--application-misc-bitmask=<n>]... [--application-misc-umask=<n>]... [--size=<string>]... [--book=<string>]...
  -F, --conffile=<file>         Specify configuration file
  --version=<str>               Specify version number
  --application-window-title=<str>      Specify window title
  --application-window-size-w=<n>
  --application-window-size-h=<n>
  --application-window-pos-x=<n>
  --application-window-pos-y=<n>
  --application-books-title=<str>
  --application-books-author=<str>
  --application-books-price=<n>
  --application-books-qty=<n>
  --application-misc-pi=<n>
  --application-misc-bigint=<n>
  --application-misc-columns=<str>
  --application-misc-bitmask=<n>
  --application-misc-umask=<n>

As you can see, conf2struct has generated `accessors’ for each setting, including nested settings. Lists can be filled by specifying the same option several times, and groups are filled by specifying several options. If a configuration file is specified, it is read first and command line options will override its settings. Otherwise, unspecified options can have default values coming from the specification file (see full documentation in the README).

Here we set the window size without a configuration file, so most of the struct is empty:

$ ./example --application-window-size-w 500 --application-window-size-h 1000
application:
    window:
        title: (null)
        size:
            w: 500
            h: 1000
        pos:
            x: 0
            y: 0
    books [0]:
    misc:
        pi: 0.000000
        bigint: 0
        columns [0]:
        bitmask: 0
        umask: 0

Here we set several books from the command line:

$ ./example --application-books-title "Lord of the Rings" --application-books-author "JRR Tolkien" --application-books-title "A song of fire and ice" --application-books-author "GRR Martin"
from command line:
version: (null)
application:
    window:
        title: (null)
        size:
            w: 0
            h: 0
        pos:
            x: 0
            y: 0
    books [2]:
        title: Lord of the Rings
        author: JRR Tolkien
        price: 0.000000
        qty: 0
        title: A song of fire and ice
        author: GRR Martin
        price: 0.000000
        qty: 0
    misc:
        pi: 0.000000
        bigint: 0
        columns [0]:
        bitmask: 0
        umask: 0

Setting groups like this is a little inconvenient: users might feel like throwing stones at us, and we don’t want that. So we can specify compound options which will set an entire group at once, based on a regular expression that explains how to parse the option value, and a mapping to the group field names. So we define compound options to set window sizes and books:

cl_groups: (
       { name: "size"; pattern: "([[:digit:]]+),([[:digit:]]+)";
         list: "application.window.size";
         targets: (
                { path: "w"; value: "$1"; },
                { path: "h"; value: "$2"; }
         );
       },
       { name: "book";
         pattern: "(.+),(.+),([0-9.]+),([[:digit:]]+)";
         list: "application.books";
        override: "title";
         targets: (
                   { path: "title"; value: "$1" },
                   { path: "author"; value: "$2" },
                   { path: "price"; value: "$3" },
                   { path: "qty"; value: "$4" }
         );
       }
)

Here we have defined a size option which takes a value composed of 2 integers separated by a coma. This will set fields in the application.window.size, in which w will take the first value matched ($1), and h will take the second value.

We also defined a book option which will set all the fields of an application.books entry. Because we specified override: "title", any command-line setting --book will check if the title field specified on the command-line matches one already set in the configuration file, and in that case it will replace that setting. Otherwise, it’ll add a new entry to the list of books.

So we can add books to those in the configuration file:

$ ./example -F example.cfg --book "Lord of the Rings","JRR Tolkien",25.95,5
from command line:
version: 1.0
application:
    window:
        title: My Application
        size:
            w: 640
            h: 480
        pos:
            x: 350
            y: 250
    books [3]:
        title: Treasure Island
        author: Robert Louis Stevenson
        price: 29.950000
        qty: 5
        title: Snow Crash
        author: Neal Stephenson
        price: 9.990000
        qty: 8
        title: Lord of the Rings
        author: JRR Tolkien
        price: 25.950000
        qty: 5
    misc:
        pi: 3.141593
        bigint: 9223372036854775807
        columns [3]:
            0:  Last Name
            1:  First Name
            2:  MI
        bitmask: 8131
        umask: 27

Or we can modify entries that were already set:

t$ ./example -F example.cfg --book "Treasure Island","Stevenson",13.95,3
from command line:
version: 1.0
application:
    window:
        title: My Application
        size:
            w: 640
            h: 480
        pos:
            x: 350
            y: 250
    books [2]:
        title: Treasure Island
        author: Stevenson
        price: 13.950000
        qty: 3
        title: Snow Crash
        author: Neal Stephenson
        price: 9.990000
        qty: 8
    misc:
        pi: 3.141593
        bigint: 9223372036854775807
        columns [3]:
            0:  Last Name
            1:  First Name
            2:  MI
        bitmask: 8131
        umask: 27

All of this is done by the tiny code in parser.c, which only calls eg_cl_parse() and then eg_print() the result:

#include <string.h>
#include <stdlib.h>
#include <libconfig.h>

#include "example.h"

void main(int argc, char* argv[]) {
    struct eg_item config, config_cl;
    const char* err;
    int res;

    res = eg_cl_parse(argc, argv, &config_cl);
    if (!res) {
        exit(1);
    }
    printf("from command line:\n");
    eg_print(&config_cl,0);
}

We just need to add example.c and argtable3.c to our build, link libconfig, and we’re good to go!