Introduction
Libconfig is a library I have used many years ago to manage the parsing and writing of a configuration file in C application. The configuration file format consists of key-value lines where the value can be integer, string, array, group, list of groups and more, so it is quite a versatile library to utilize in your C application. In this post, I will share a quick libconfig tutorial to demonstrate the most common use cases of this library so you can quickly get started.
Refer to official libconfig project page here to learn more.
Parse a Config File
Consider this config file (/myconfig.conf) written in a style that libconfig can understand, where:
- curly brackets
{}
denotes a group - regular brackets
()
denotes a list - square brackets
[]
denotes an array - A key – value pair forms a configuration parameter. If the value is a string, you need to use double quotes (
""
) to surround it.
MYCONFIG = { SERVER_IP = "123.456.789.1"; SERVER_PORT = 8086; USERINFO = { USERNAME = "cary"; USERTOKENS = ["tok1", "tok2"]; }; TYPES = ( { ID = 1; NAME = "type1"; }, { ID = 2; NAME = "type2"; SPECIAL = 1; }, { ID = 3; NAME = "type3"; } ); };
libconfig is able to parse all types of configuration types listed above with the following C code:
#include "libconfig.h" #include <stdio.h> struct userinfo { char username[128]; int usertokennum; char usertokens[64][128]; } struct types { int id; char name[128]; int special; } struct myconfig { char server_ip[128]; int server_port; struct userinfo userinfo; int numtypes; struct types types[20]; } int loadConfParams(void) { config_t conf; int tmpInt = 0; const char * tmpStr = NULL; unsigned int i = 0; config_setting_t * spSetting = NULL; config_setting_t * spTypes = NULL; config_setting_t * member = NULL; struct myconfig myconfig; config_init(&conf); if(!config_read_file(&conf, "/myconfig.conf")) { printf("error file = %s, error line = %d, error text = %s", config_error_file(&conf), config_error_line(&conf), config_error_text(&conf)); config_destroy(&conf); return -1; } if(config_lookup_string(&conf,"MYCONFIG.SERVER_IP",&tmpStr)) { printf("%s=%s\n", "MYCONFIG.SERVER_IP", tmpStr); strncpy(&myconfig.server_ip[0], tmpStr, strlen(tmpStr)); } else { printf("no MYCONFIG.SERVER_IP\n"); } if(config_lookup_int(&conf,"MYCONFIG.SERVER_PORT",&tmpInt)) { printf("%s=%s\n", "MYCONFIG.SERVER_PORT", tmpInt); myconfig.server_port = tmpInt; } else { printf("no MYCONFIG.SERVER_PORT\n"); } if(config_lookup_string(&conf,"MYCONFIG.USERINFO.USERNAME",&tmpStr)) { printf("%s=%s\n", "MYCONFIG.USERINFO.USERNAME", tmpStr); strncpy(&myconfig.userinfo.username[0], tmpStr, strlen(tmpStr)); } else { printf("no MYCONFIG.USERINFO.USERNAME\n"); } spSetting = config_lookup(&conf, "MYCONFIG.USERINFO.USERTOKENS"); if (spSetting && config_setting_length(spSetting) != 0) { printf("number of MYCONFIG.USERINFO.USERTOKENS = %d",spSetting->value.list->length); myconfig.userinfo.usertokennum = spSetting->value.list->length > 64 ? 64 : spSetting->value.list->length; for(i = 0; i < myconfig.userinfo.usertokennum; i++) { strncpy(&myconfig.userinfo.usertokennum[i][0], spSetting->value.list->elements[i]->value.sval, strlen(spSetting->value.list->elements[i]->value.sval); printf("%s", &myconfig.userinfo.usertokennum[i][0]); } } spSetting = config_lookup(&conf, "MYCONFIG"); if (spSetting) { spTypes = config_setting_get_member(spSetting, "TYPES"); if (spTypes) { printf("there are %d types\n", config_setting_length(spTypes)); myconfig.numtypes = config_setting_length(spTypes); for(i = 0; i < myconfig.numtypes; i++) { member = config_setting_get_elem(spTypes, i); if (member) { if(config_setting_lookup_int(member, "ID", &tmpInt)) { myconfig.types[i].id = tmpInt; } else { printf("no ID\n"); } if(config_setting_lookup_string(member, "NAME", &tmpStr)) { strncpy(&(myconfig.types[i].name[0]), tmpStr, strlen(tmpStr)); } else { printf("no NAME\n"); } if(config_setting_lookup_int(member, "SPECIAL", &tmpInt)) { myconfig.types[i].special = tmpInt; } else { printf("no SPECIAL\n"); } } } } } config_destroy(&conf); return 0; }
Initialization
Before we can read the configuration file and start parsing it, we need to initialize a config_t
structure. which holds everything needed to successfully parse the file. This block of code illustrates this:
config_init(&conf); if(!config_read_file(&conf, "/myconfig.conf")) { printf("error file = %s, error line = %d, error text = %s", config_error_file(&conf), config_error_line(&conf), config_error_text(&conf)); config_destroy(&conf); return -1; }
Process Configuration Members
Then we are ready to look up configuration parameters by their paths
. This path is denoted by the key
values stated in the configuration file and a dot (.
) is used to denote the hierarchy of the parameters. For example, if you have a group name A that has an integer value B inside, you would access the integer value C as A.B
.
This code block is to read a string parameter:
if(config_lookup_string(&conf,"MYCONFIG.SERVER_IP",&tmpStr)) { printf("%s=%s\n", "MYCONFIG.SERVER_IP", tmpStr); strncpy(&myconfig.server_ip[0], tmpStr, strlen(tmpStr)); } else { printf("no MYCONFIG.SERVER_IP\n"); }
This code block is to read an integer parameter:
if(config_lookup_int(&conf,"MYCONFIG.SERVER_PORT",&tmpInt)) { printf("%s=%s\n", "MYCONFIG.SERVER_PORT", tmpInt); myconfig.server_port = tmpInt; } else { printf("no MYCONFIG.SERVER_PORT\n"); }
The result of the reading (if exists) will be stored in tmpInt
and tmpStr
pointers and you are free to do whatever you want with the values.
Process Array Configuration Type
Array configuration type requires different technique to parse. We will have to use a config_setting_t
pointer to access the array configuration member. This strucutre holds the size of this array (using config_setting_length
) and the value of each element:
- spSetting->value.list->elements[i]->value.sval for string values
- spSetting->value.list->elements[i]->value.ival for integer values
This block of code demonstrates this technique with libconfig:
spSetting = config_lookup(&conf, "MYCONFIG.USERINFO.USERTOKENS"); if (spSetting && config_setting_length(spSetting) != 0) { printf("number of MYCONFIG.USERINFO.USERTOKENS = %d",spSetting->value.list->length); myconfig.userinfo.usertokennum = spSetting->value.list->length > 64 ? 64 : spSetting->value.list->length; for(i = 0; i < myconfig.userinfo.usertokennum; i++) { strncpy(&myconfig.userinfo.usertokennum[i][0], spSetting->value.list->elements[i]->value.sval, strlen(spSetting->value.list->elements[i]->value.sval); printf("%s", &myconfig.userinfo.usertokennum[i][0]); } }
Process List Configuration Type
A List is different from an array because it can contain different data types as its members while array cannot. It can consist of groups, integers, another lists or strings in any combinations you want. The example above parses a list of groups where we make config_lookup
to obtain the entire configuration parameters as config_setting_t pointer. From this setting, we then locate the list by a call to config_setting_get_member
. This function will return another config_setting_t structure in which we can access each member in the list. A call to config_setting_get_elem
will return a config_setting_t pointer that represent the group, from which we can access key-value pairs in the same techniques as before.
spSetting = config_lookup(&conf, "MYCONFIG"); if (spSetting) { spTypes = config_setting_get_member(spSetting, "TYPES"); if (spTypes) { printf("there are %d types\n", config_setting_length(spTypes)); myconfig.numtypes = config_setting_length(spTypes); for(i = 0; i < myconfig.numtypes; i++) { member = config_setting_get_elem(spTypes, i); if (member) { if(config_setting_lookup_int(member, "ID", &tmpInt)) { myconfig.types[i].id = tmpInt; } else { printf("no ID\n"); } if(config_setting_lookup_string(member, "NAME", &tmpStr)) { strncpy(&(myconfig.types[i].name[0]), tmpStr, strlen(tmpStr)); } else { printf("no NAME\n"); } if(config_setting_lookup_int(member, "SPECIAL", &tmpInt)) { myconfig.types[i].special = tmpInt; } else { printf("no SPECIAL\n"); } } } } }
Clean up
At the end of the configuration file reading, we need to make a call to config_destroy(&conf)
to wrap up the session.
Write or Update a Config File
Writing to a file shares similar coding formats as reading from it , with slight difference of course. The following example attempts to update parameters if they differ from what it has on hand. This is simple, but when it comes to array data type, the logic tends to get more complicated. This is because there are several conditions you can check for “difference” such as number of elements, values of the elements etc. Consider the following example:
#include "libconfig.h" #include <stdio.h> /* new values */ char * serverip = "777.888.999.000"; int server_port = 1234; char tokens[5] = ["tok1", "tok2", "tok3", "tok4", "tok5"]; int numtokens = 5; int updateConfParams(void) { config_t conf; int tmpInt, i = 0; const char * tmpStr = NULL; config_setting_t *confsetting = NULL; config_setting_t *array = NULL; config_init(&scuconf); /* load config file */ if(!config_read_file(&scuconf, "/myconfig.conf")) { printf("config_read_file failed, error file = %s, error line = %d, error text = %s", config_error_file(&conf), config_error_line(&conf), config_error_text(&conf)); config_destroy(&conf); return -1; } /* update SERVER_IP */ if(config_lookup_string(&conf,"MYCONFIG.SERVER_IP",&tmpStr)) { if (strcasecmp(tmpStr, serverip)) { /* different value, needs update */ printf("updating MYCONFIG.SERVER_IP from %s to %s\n", tmpStr, serverip); confsetting = config_lookup(&conf, "MYCONFIG.SERVER_IP"); config_setting_set_string(confsetting, serverip); } else { printf("no need to update MYCONFIG.SERVER_IP\n"); } } else { printf("no MYCONFIG.SERVER_IP\n"); } /* update SERVER_PORT */ if(config_lookup_int(&conf,"MYCONFIG.SERVER_PORT",&tmpInt)) { if (tmpInt != server_port) { /* different value, needs update */ printf("updating MYCONFIG.SERVER_PORT from %d to %d\n", tmpInt, server_port); confsetting = config_lookup(&conf, "MYCONFIG.SERVER_PORT"); config_setting_set_int(confsetting, server_port); } else { printf("no need to update MYCONFIG.SERVER_PORT\n"); } } else { printf("no MYCONFIG.SERVER_PORT\n"); } /* update USERTOKENS array */ array = config_lookup(&scuconf, "MYCONFIG.USERINFO.USERTOKENS"); if (array) { if (config_setting_length(array) == 0) { /* if the array config has zero elements, we need to updaet it for sure */ for (int i = 0; i < numtokens; i++) { printf("updating MYCONFIG.USERINFO.USERTOKENS at index %d to %s", i, tokens[i]); config_setting_set_string_elem(array, -1, tokens[i]); } } else if (config_setting_length(array) > 0 && config_setting_length(array) != numtokens) { /* number of elements differ. Have to update for sure */ for(int i = 0; i < array->value.list->length; i++) { /* remove current elements */ config_setting_remove_elem(array, i); } for (int i = 0; i < numtokens; i++) { printf("updating MYCONFIG.USERINFO.USERTOKENS at index %d to %s", i, tokense[i]); config_setting_set_string_elem(array, -1, tokens[i]); } } else { /* number of elements are the same. check one by one */ for (int i = 0; i < numtokens; i++) { if (strcasecmp(array->value.list->elements[i]->value.sval, tokense[i])) { printf("updating MYCONFIG.USERINFO.USERTOKENS at index %d to %s", i, tokense[i]); config_setting_set_string_elem(array, i, tokense[i]); } } } } /* write new settings to config file */ if(!(config_write_file(&scuconf, "/myconfig.conf"))) { printf("Error writing config file %s\n", "/myconfig.conf"); } config_destroy(&scuconf); return 0; }
Update an Integer and String Value
This is fairly simple, before we update, we first read the current values in the config file, and compare them with the new values we have. If the values are the same, there is no need to update. If the values differ, we will have to obtain a reference to config_setting_t*
so we can invoke config_setting_set_string()
or config_setting_set_int
to update the values respectively.
This code block illustrates integer value update:
/* update SERVER_PORT */ if(config_lookup_int(&conf,"MYCONFIG.SERVER_PORT",&tmpInt)) { if (tmpInt != server_port) { /* different value, needs update */ printf("updating MYCONFIG.SERVER_PORT from %d to %d\n", tmpInt, server_port); confsetting = config_lookup(&conf, "MYCONFIG.SERVER_PORT"); config_setting_set_int(confsetting, server_port); } else { printf("no need to update MYCONFIG.SERVER_PORT\n"); } } else { printf("no MYCONFIG.SERVER_PORT\n"); }
This code block illustrates string value update:
/* update SERVER_IP */ if(config_lookup_string(&conf,"MYCONFIG.SERVER_IP",&tmpStr)) { if (strcasecmp(tmpStr, serverip)) { /* different value, needs update */ printf("updating MYCONFIG.SERVER_IP from %s to %s\n", tmpStr, serverip); confsetting = config_lookup(&conf, "MYCONFIG.SERVER_IP"); config_setting_set_string(confsetting, serverip); } else { printf("no need to update MYCONFIG.SERVER_IP\n"); } } else { printf("no MYCONFIG.SERVER_IP\n"); }
Update an Array Structure
The update of a configuration parameter of type array is slightly more complicated if we were to update it only if it differs from the new values. There are several comparisons we have to make:
- do number of elements in the configuration the same as the new array?
- if the number elements are the same, we need to check one by one.
We access the array configuration, we will also need a reference to config_setting_t
, which would represent the array. We can then use APIs like config_setting_length
to get the number of elements in the array and that is the first thing we can compare.
If number of elements differ, we can use config_setting_remove_elem
to remove the old element and use config_setting_set_string_elem
to write out the new array element. Please note that the removal of the old element is important otherwise it would not be updated correctly. Below is the code block for array update:
/* update USERTOKENS array */ array = config_lookup(&scuconf, "MYCONFIG.USERINFO.USERTOKENS"); if (array) { if (config_setting_length(array) == 0) { /* if the array config has zero elements, we need to updaet it for sure */ for (int i = 0; i < numtokens; i++) { printf("updating MYCONFIG.USERINFO.USERTOKENS at index %d to %s", i, tokens[i]); config_setting_set_string_elem(array, -1, tokens[i]); } } else if (config_setting_length(array) > 0 && config_setting_length(array) != numtokens) { /* number of elements differ. Have to update for sure */ for(int i = 0; i < array->value.list->length; i++) { /* remove current elements */ config_setting_remove_elem(array, i); } for (int i = 0; i < numtokens; i++) { printf("updating MYCONFIG.USERINFO.USERTOKENS at index %d to %s", i, tokense[i]); config_setting_set_string_elem(array, -1, tokens[i]); } } else { /* number of elements are the same. check one by one */ for (int i = 0; i < numtokens; i++) { if (strcasecmp(array->value.list->elements[i]->value.sval, tokense[i])) { printf("updating MYCONFIG.USERINFO.USERTOKENS at index %d to %s", i, tokense[i]); config_setting_set_string_elem(array, i, tokense[i]); } } } }
Write the Config File and Clean up
When all the updates have been done, we can then update the physical config file and clean up the resources like this:
/* write new settings to config file */ if(!(config_write_file(&scuconf, "/myconfig.conf"))) { printf("Error writing config file %s\n", "/myconfig.conf"); } config_destroy(&scuconf);
Recommended Posts
- Create Custom Data Types With the C Struct
- Build Your First C Program with Basic Makefile Structure
- A Closer Look at C Data Types and Variables
Hi, this is Cary, your friendly tech enthusiast, educator and author. Currently working as a software architect at Highgo Software Canada. I enjoy simplifying complex concepts, diving into coding challenges, unraveling the mysteries of software. Most importantly, I like sharing and teaching others about all things tech. Find more blogs from me at highgo.ca