Inspired by sj.h which implements JSON in 150 lines of code.
- A single header file.
- Just 16 LOC for parsing standard INI files.
- +32 LOC for optional nested array and dictionary support.
- No memory allocations.
- Note that parsing modifies the underlying buffer to insert '\0' bytes. Make sure the parsed string is writeable.
- Very basic error checking: returns 0 if invalid INI was encountered, returns 1 if it was valid.
To parse this file.
; This is a comment
[window]
width = 800
height = 600
We only need to call the parse_ini
function in a loop.
parse_ini takes a pointer to the file buffer, as well as various string pointers to which it will write the parsed sectors, keys and values.
char *cursor = file_contents, *sector = NULL, *key = NULL, *value = NULL;
while(*cursor != '\0')
{
if(!parse_ini(&cursor, §or, &key, &value)){ printf("INI syntax error when parsing sector \"%s\", key \"%s\", value \"%s\".", sector, key, value); }
if(key == NULL && value == NULL){ /* New sector */}
// Basic values
if(strcmp(sector, "window") == 0)
{
if(strcmp(key, "width") == 0) config.window_width = atoi(value);
if(strcmp(key, "height") == 0) config.window_height = atoi(value);
/* etc. */
}
}
Normal INI files do not support arrays and dictionaries. We can optionally support them by calling the parse_ini_array
and parse_ini_dict
functions.
These functions need some space to write the keys and values to. The last element is how many elements the passed-in arrays can hold.
[app]
file_history = ["/user/files/abc.png", "/user/files/foo.png", "/user/files/bar.png"]
keymap = {"ctrl+a" = "select_all", "tab" = "open_menu"}
char *cursor = file_contents, *sector = NULL, *key = NULL, *value = NULL;
while(*cursor != '\0')
{
if(!parse_ini(&cursor, §or, &key, &value)){ printf("INI syntax error when parsing sector \"%s\", key \"%s\", value \"%s\".", sector, key, value); }
if(strcmp(sector, "app") == 0)
{
if(strcmp(key, "file_history") == 0)
{
char* items[16]; int length = 0;
if(!parse_ini_array(value, items, &length, 16)){ printf("INI syntax error when parsing array items in \"%s\"/\"%s\" value \"%s\".", sector, key, value); }
for(int i = 0; i < length; i++)
app.file_history[i] = items[i];
app.num_file_history_items = length;
}
if(strcmp(key, "keymap") == 0)
{
char* keys[128]; char* values[128]; int length = 0;
if(!parse_ini_dict(value, keys, values, &length, 128)){ printf("INI syntax error when parsing dictionary items in \"%s\"/\"%s\" value \"%s\".", sector, key, value); }
for(int i = 0; i < length; i++)
dict_insert_value(app.keymap, keys[i], values[i]);
}
/* etc. */
}
}
The parse_ini_array and parse_ini_dict functions also handle nesting.
Just call them again on the value returned if that value starts with '['
or '{'
respectively. If you don't know the exact depth beforehand, for for example serialization or deserialization you can use recursion.
[network]
blacklist = [{ip=192.168.132.8, expires=01/10/2026, issuer=Matthew, comment="Go away Darren."}, {ip=192.168.132.7, expires=01/01/2099, issuer=Darren, comment="Until Hell freezes over."}]
if(strcmp(sector, "network") == 0)
{
if(strcmp(key, "blacklist") == 0)
{
char* items[256]; int length = 0;
if(!parse_ini_array(value, items, &length, 16)){ printf("INI syntax error when parsing array items in \"%s\"/\"%s\" value \"%s\".", sector, key, value); }
for(int i = 0; i < length; i++)
{
// Each item is a dictionary
char* subkeys[8]; char* subvalues[8]; int sublen = 0;
if(!parse_ini_dict(items[i], subkeys, subvalues, &sublen, 8)){ printf("INI syntax error when parsing dictionary items in \"%s\"/\"%s\" array item \"%s\".", sector, key, items[i]); }
char* ip = NULL, *expires = NULL, *issuer = NULL, *comment = NULL;
for(int j = 0; j < sublen; j++)
{
if(strcmp(subkeys[j], "ip") == 0) ip = subvalues[j];
if(strcmp(subkeys[j], "expires") == 0) expires = subvalues[j];
if(strcmp(subkeys[j], "issuer") == 0) issuer = subvalues[j];
if(strcmp(subkeys[j], "comment") == 0) comment = subvalues[j];
}
network.blacklist[i] = {ip, expires, issuer, comment};
}
network.num_blacklist_items = length;
}
/* etc. */
}