Simple Key Value Text Parser In C
posts no malloc C Coding Programming Key ValueFor linux if you call env
you will get a sequence of bash variable like:
XDG_SESSION_DESKTOP=cinnamon
QT_QPA_PLATFORMTHEME=qt5ct
XDG_SESSION_TYPE=x11
What if you want to parse such files in C? I written a kv_parse_value()
which might help you!
It has zero dependencies, doesn't require malloc and also can handle whitespace and quoted strings.
Typically application is linux software, embedded systems, simple configuration store... etc...
If this helps you, please do let me know!
Also it's now in github at https://github.com/mofosyne/kv_parse_value.c
In addition, I noticed that for very simple INI files this will be able to parse it as well!
You could potentially also parse some linux program output which uses a similar form...
but for those kind of unstructured output it would tend to have whitespace and use :
instead of =
.
I have intentionally written this in a way that you can easily modify and strip it down, in case you are trying to run this in an embedded or restricted context.
Other Style Supported
Spaces and ':'
XDG_SESSION_DESKTOP : cinnamon
QT_QPA_PLATFORMTHEME : qt5ct
XDG_SESSION_TYPE : x11
Quotes and Double Quotes
XDG_SESSION_DESKTOP="cinnamon"
QT_QPA_PLATFORMTHEME="qt5ct"
XDG_SESSION_TYPE="x11"
Key Features:
- ASCII Support Only: The function only processes standard ASCII characters. Non-ASCII input may produce undefined behavior.
- Malloc-Free: No dynamic memory allocation is used. The caller must provide a fixed-size buffer.
- String-Only Values: Extracted values are stored as null-terminated C strings.
- Whitespace Handling (
KV_PARSE_WHITESPACE_SKIP
):- Ignores leading and trailing spaces/tabs around keys and values.
- Quoted String Support (
KV_PARSE_QUOTED_STRINGS
):- Allows values to be enclosed in single (
'
) or double ("
) quotes. - Handles escaped quotes within quoted values (
"example\"text"
).
- Allows values to be enclosed in single (
- Multi-Character Delimiter Support:
- Accepts both
=
and:
as key-value separators (e.g.,"key=value"
or"key: value"
).
- Accepts both
- Line-Based Parsing:
- The function skips to the next line if the key does not match.
- Safe String Copy:
- Ensures the extracted value does not exceed
value_max - 1
characters.
- Ensures the extracted value does not exceed
Source
/* Simple ANSI C KV Parser
* 2025 Brian Khuu (This Code Is Released To The Public Domain)
* To test this function run `cat kvparser.h | tcc -DKV_PARSER_TEST -run -` */
#define KV_PARSE_WHITESPACE_SKIP
#define KV_PARSE_QUOTED_STRINGS
/**
* @brief Parses a key-value pair from a given string.
*
* 2025 Brian Khuu https://briankhuu.com/ (This function is dedicated to Public Domain)
* https://briankhuu.com/blog/2025/01/30/simple-key-value-text-parser-in-c/
*
* This function extracts the value associated with a specified key in a formatted key-value string.
*
* ## Supported Features:
* - **Whitespace Skipping** (`KV_PARSE_WHITESPACE_SKIP`): Ignores spaces and tabs around keys and values.
* - **Quoted String Support** (`KV_PARSE_QUOTED_STRINGS`): Handles values enclosed in single (`'`) or double (`"`) quotes.
* - **Key Delimiters**: Supports both `=` and `:` as key-value separators.
*
* @param str Input string containing multiple key-value pairs (e.g., "key=value") separated by newline.
* @param key The key to search for in the input string.
* @param value Output buffer to store the extracted value.
* @param value_max Maximum length of the output buffer (`value`), including the null terminator.
* @return The length of the extracted value (excluding the null terminator) on success, or `0` if the key is not found.
*
* @note If `KV_PARSE_WHITESPACE_SKIP` is defined, leading and trailing whitespace is ignored before returning the value.
* @note If `KV_PARSE_QUOTED_STRINGS` is defined, values can be enclosed in single (`'`) or double (`"`) quotes.
*
* @example Usage Example:
* @code
* char buffer[50];
* unsigned int len = kv_parse_value("username=admin\npassword=1234", "username", buffer, sizeof(buffer));
* if (len > 0) {
* printf("Username: %s\n", buffer);
* } else {
* printf("Key not found.\n");
* }
* @endcode
*/
unsigned int kv_parse_value(const char *str, const char *key, char *value, unsigned int value_max)
{
for (; *str != '\0'; str++)
{
#ifdef KV_PARSE_WHITESPACE_SKIP
while (*str == ' ' || *str == '\t')
{
str++;
}
#endif
/* Check For Key */
for (int i = 0; *str != '\0' && key[i] != '\0'; i++, str++)
{
/* Key Mismatched. Skip Line */
if (*str != key[i])
{
goto skip_line;
}
}
#ifdef KV_PARSE_WHITESPACE_SKIP
while (*str == ' ' || *str == '\t')
{
str++;
}
#endif
/* Check For Key Value Delimiter */
if (*str != '=' && *str != ':')
{
goto skip_line;
}
str++;
#ifdef KV_PARSE_WHITESPACE_SKIP
while (*str == ' ' || *str == '\t')
{
str++;
}
#endif
/* Copy Value To Buffer */
#ifdef KV_PARSE_QUOTED_STRINGS
char quote = '\0';
#endif
for (int i = 0; i < (value_max - 1); str++)
{
if (*str == '\0' || *str == '\r' || *str == '\n')
{
/* End Of Line. Trim trailing whitespace before returning the value. */
value[i] = '\0';
#ifdef KV_PARSE_WHITESPACE_SKIP
while (i > 0 && (value[i - 1] == ' ' || value[i - 1] == '\t'))
{
i--;
value[i] = '\0';
}
#endif
return i;
}
#ifdef KV_PARSE_QUOTED_STRINGS
else if (quote == '\0' && (*str == '\'' || *str == '"'))
{
/* Start Of Quoted String */
quote = *str;
continue;
}
else if (quote != '\0' && *(str - 1) != '\\' && *str == quote)
{
/* End Of Quoted String. Return Value */
value[i] = '\0';
return i;
}
else if (quote != '\0' && *(str - 1) == '\\' && *str == quote)
{
/* Escaped Quote Character In Quoted String */
value[i - 1] = *str;
continue;
}
#endif
value[i++] = *str;
}
/* Value too large for buffer. Don't return a value. */
value[0] = '\0';
return 0;
skip_line:
/* Search for start of next line */
while (*str != '\n')
{
if (*str == '\0')
{
/* End of string. Key was not found. */
return 0;
}
str++;
}
}
/* End of string. Key was not found. */
return 0;
}
#ifdef KV_PARSER_TEST
#include <assert.h>
#include <stdio.h>
#include <string.h>
// Test case function
void run_tests()
{
char buffer[100] = {0};
int buffer_count = 0;
// **Test 1: Basic Key-Value Retrieval**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("key1=value1\nkey2=value2", "key1", buffer, sizeof(buffer));
assert(buffer_count == 6);
assert(strcmp(buffer, "value1") == 0);
// **Test 2: Retrieve Last Key**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("a=b\nc=d\ne=f\ng=hello", "g", buffer, sizeof(buffer));
assert(buffer_count == 5);
assert(strcmp(buffer, "hello") == 0);
// **Test 3: Key Not Found**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("a=b\nc=d", "z", buffer, sizeof(buffer));
assert(buffer_count == 0);
// **Test 4: Buffer Too Small**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("longkey=longvalue", "longkey", buffer, sizeof(buffer));
assert(buffer_count == 9);
assert(strcmp(buffer, "longvalue") == 0);
// **Test 5: Empty Input**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("", "anykey", buffer, sizeof(buffer));
assert(buffer_count == 0);
// **Test 6: Input Without Key-Value Pairs**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("randomtext\nanotherline", "key", buffer, sizeof(buffer));
assert(buffer_count == 0);
// **Test 7: Handling Spaces Around Key and Value**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value(" key = value \n next = test ", "key", buffer, sizeof(buffer));
#ifdef KV_PARSE_WHITESPACE_SKIP
assert(buffer_count == 5);
assert(strcmp(buffer, "value") == 0);
#else
assert(buffer_count == 0);
#endif
// **Test 8: Duplicate Keys (Return First Occurrence)**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("x=1\nx=2\nx=3", "x", buffer, sizeof(buffer));
assert(buffer_count == 1);
assert(strcmp(buffer, "1") == 0);
// **Test 9: Newline Variations (Windows vs. Unix)**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("a=one\r\nb=two", "b", buffer, sizeof(buffer));
assert(buffer_count == 3);
assert(strcmp(buffer, "two") == 0);
// **Test 10: Key With Special Characters**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("user-name=admin\nuser@domain.com=me", "user-name", buffer, sizeof(buffer));
assert(buffer_count == 5);
assert(strcmp(buffer, "admin") == 0);
// **Test 11: Value Containing '='**
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("path=/home/user=data", "path", buffer, sizeof(buffer));
assert(buffer_count == 15);
assert(strcmp(buffer, "/home/user=data") == 0);
// **Test 12: Quoted String **
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("path=\"/home/user=data\"", "path", buffer, sizeof(buffer));
#ifdef KV_PARSE_QUOTED_STRINGS
assert(buffer_count == 15);
assert(strcmp(buffer, "/home/user=data") == 0);
#else
assert(buffer_count == 17);
assert(strcmp(buffer, "\"/home/user=data\"") == 0);
#endif
// **Test 13: Uncapped Quoted String **
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("path=\"/home/user=data", "path", buffer, sizeof(buffer));
#ifdef KV_PARSE_QUOTED_STRINGS
assert(buffer_count == 15);
assert(strcmp(buffer, "/home/user=data") == 0);
#else
assert(buffer_count == 16);
assert(strcmp(buffer, "\"/home/user=data") == 0);
#endif
// **Test 14: Quoted String With Escaped Quote **
memset(buffer, 0, sizeof(buffer));
buffer_count = kv_parse_value("path=\"/home/\\\"user=data\"", "path", buffer, sizeof(buffer));
#ifdef KV_PARSE_QUOTED_STRINGS
assert(buffer_count == 16);
assert(strcmp(buffer, "/home/\"user=data") == 0);
#else
assert(buffer_count == 19);
assert(strcmp(buffer, "\"/home/\\\"user=data\"") == 0);
#endif
printf("kv_parse_value() passed successfully!\n");
}
// Run tests in main()
int main()
{
run_tests();
return 0;
}
#endif
When you run the above source in tcc via cat kvparser.h | tcc -DKV_PARSER_TEST -run -
kv_parse_value() passed successfully!