Ini-Datei-Parser mit Callback-Funktion in C
Ini file parser with callback function in C
This little piece of header and source code is a ini file (or string)
parser, small enough to be used efficiently in embedded programming
context. All it does is to read the file in one go and invoke a callback
that you define as function pointer argument. Parameters you get in this
function are the section, the key and the value as string, as well as a
void*
to a variable that you define. It will be passed through the parser
to your function and can be used e.g. as location of your configuration
variables. By default the parser sanatizes sections, keys and values:
- It strips whitespaces from keys, values and sections
- It transforms keys and sections to lowercase
- It "inline" ignores comments (after the value)
- It handles UTF-8 BOM at the beginning of the file
Using some flags you can disable these features individually. A flag also allows you to parse an ini-formatted string instead of reading from file.
There is nothing more about it - you can find a sample source code, Make file, example ini file and the output of this program below :)
Dieses kleine Stück Quelltext ist ein Inidatei-Parser (oder Ini-String-Parser),
der klein genug für den Einsatz im Embedded-Kontext ist. Er liest die INI-Datei
zeilenweise in einem Durchgang und ruft für jeden Eintrag eine definierte
Callback-Funktion auf, welche ihm als Argument übergeben wird. Die Callback
erhält als Argumente die Sektion, den Key, den Wert (alle als C-String), sowie
einen void*
, der durch den Parser geschleift wird und z.B. den Speicherort
der ausgewerteten Einträge bestimmt. "By default" werden die Sektionsnamen,
Keys und Werte bereits vor Aufrufen der Callback aufbereitet:
- Leerzeichen/Tabs werden vor und hinter Sektionen, Keys und Werten entfernt
- Sektionen und Keys werden in Kleinschreibweise transformiert
- Kommentare in Eintragszeilen werden entfernt bzw. ignoriert
- Die UTF-8 BOM-Information am Anfang der Dateien werden behandelt
Mit Flags kann jede dieser Funktionen individuell abgeschaltet werden. Ebenso dient ein Flag dazu das erste Parser-Funktionsargument als INI-formatierten text zu interpretieren und nicht als Dateipfad.
Mehr ist es nicht - ein Beispiel mit Quelltext, Makefile, Ini-Datei und Ausgabe steht weiter unten :)
Header
/********************************************************************
* @file ini_parse.h
*
* @author: Stefan Wilhelm
* @license: LGPL 2
*
* ------------------------------------------------------------------
*
* Simple parser for initialisation file data. Can read from file or
* string.
*
*
********************************************************************/
#ifndef __INI_PARSER_H__
#define __INI_PARSER_H__
#ifdef __cplusplus
extern "C" {
#endif
/**
* Error return values for parse_ini()
*
* @enum ini_parse_error
*/
enum ini_parse_error {
INI_PARSER_ERROR_NO = 0,
INI_PARSER_ERROR_OPEN_FAILED,
INI_PARSER_ERROR_INVALID_PARSER_STATE,
INI_PARSER_ERROR_MISSING_SECTION_CLOSE_BRACKET,
INI_PARSER_ERROR_SECTION_TOO_LONG,
INI_PARSER_ERROR_KEY_TOO_LONG,
INI_PARSER_ERROR_VALUE_TOO_LONG,
INI_PARSER_ERROR_MISSING_VALUE,
INI_PARSER_ERROR_NULLPOINTER,
INI_PARSER_ERROR_UNKNOWN
};
/**
* Returns a description for a given ini parser error code.
*
* @param enum ini_parse_error err
* @return const char*
*/
const char *ini_parse_err_str(int);
/**
* Flags for parse_ini()
*
* @enum parse_ini_flags
*/
enum parse_ini_flags {
INI_PARSER_FLAG_DONT_LOWERCASE_SECTIONS_AND_KEYS = 0x01,
INI_PARSER_FLAG_DONT_TRIM_SECTIONS_AND_KEYS = 0x02,
INI_PARSER_FLAG_DONT_TRIM_VALUES = 0x04,
INI_PARSER_FLAG_DONT_ALLOW_BOM = 0x08,
INI_PARSER_FLAG_DONT_ALLOW_COMMENTS_AFTER_VALUES = 0x10,
INI_PARSER_FLAG_PARSE_STRING = 0x20
};
/**
* Callback type, invoked every time a key value pair is determined in
* function parse_ini(). Passed arguments are section, key, value and a custom
* data pointer defined when invoking parse_ini(). Latter is used, e.g. to
* define where to set parsed data.
*
* @param const char *section
* @param const char *key
* @param const char *value
* @param void *callback_data
*/
typedef int (*parse_ini_callback_t)(const char *, const char *, const char *, void *);
/**
* Parses initialisation file format data from text or file, dependent on the
* INI_PARSER_FLAG_PARSE_STRING flag. Invokes the callback every time a key
* value pair is set.
*
* @param const char *file_or_string
* @param parse_init_callback_t callback
* @param void *callack_data
* @param enum parse_ini_flags flags
* @return enum ini_parse_error
*/
int ini_parse(const char* file_or_string, parse_ini_callback_t callback, void *callack_data, unsigned int flags);
#ifdef __cplusplus
}
#endif
#endif
Implemation
/********************************************************************
* @file ini_parse.h
*
* @author: Stefan Wilhelm
* @license: LGPL 2
*
* ------------------------------------------------------------------
*
* Simple parser for initialisation file data. Can read from file or
* string.
*
********************************************************************/
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "ini_parse.h"
#define INI_PARSER_MAX_KEY_LEN (64)
#define INI_PARSER_MAX_SECTION_LEN (64)
#define INI_PARSER_MAX_VALUE_LEN (128)
/**
* Registered ini parser error codes
*/
const char *ini_parse_error_strings[] = {
"Ok",
"Failed to open ini file",
"Invalid ini parser state",
"Missing ini section closing bracket",
"Ini section name too long",
"Ini key name too long",
"Ini value text too long",
"Ini value missing",
"Null pointer passed to ini parser!",
"Unknown Error",
"",""
};
/**
* Returns a description for a given ini parser error code.
*
* @param enum ini_parse_error err
* @return const char*
*/
const char *ini_parse_err_str(int err)
{
if(err < INI_PARSER_ERROR_NO || err > INI_PARSER_ERROR_UNKNOWN) {
err = INI_PARSER_ERROR_UNKNOWN;
}
return ini_parse_error_strings[err];
}
/**
* Internal enum for parser states
*/
typedef enum {
state_init = 0,
state_comment,
state_section,
state_key,
state_value
} state_t;
/**
* Parses initialisation file format data from text or file, dependent on the
* INI_PARSER_FLAG_PARSE_STRING flag. Invokes the callback every time a key
* value pair is set.
*
* @param const char *file_or_string
* @param parse_init_callback_t callback
* @param void *callack_data
* @param enum parse_ini_flags flags
* @return enum ini_parse_error
*/
int ini_parse(const char* file_or_string, parse_ini_callback_t callback, void *callack_data, unsigned int flags)
{
FILE *p_file;
int error = INI_PARSER_ERROR_NO;
char section[INI_PARSER_MAX_SECTION_LEN] = "";
char key[INI_PARSER_MAX_KEY_LEN] = "";
char value[INI_PARSER_MAX_VALUE_LEN] = "";
char ch = '\0';
size_t pos = 0, line = 0, character = 0;
size_t text_last_char = strlen(file_or_string)-1;
state_t state = state_init;
if(!file_or_string) {
return INI_PARSER_ERROR_NULLPOINTER;
} else if (!(flags & INI_PARSER_FLAG_PARSE_STRING) && !(p_file = fopen(file_or_string, "r"))) {
return INI_PARSER_ERROR_OPEN_FAILED;
}
while(!error) {
if(flags & INI_PARSER_FLAG_PARSE_STRING) {
if(character >= text_last_char || (ch = file_or_string[character]) == '\0') {
break;
}
} else {
if(feof(p_file) || !(ch=fgetc(p_file))) {
break;
}
}
character++;
switch(state) {
case state_init:
if((!(flags & INI_PARSER_FLAG_DONT_ALLOW_BOM)) && (character < 4) && (
(character == 1 && (unsigned char)ch == 0xEF) ||
(character == 2 && (unsigned char)ch == 0xBB) ||
(character == 3 && (unsigned char)ch == 0xBF))
) {
continue;
} else if(ch == '\n' || ch == '\r' || isspace(ch)) {
line += ch == '\n' ? 1 : 0;
continue;
} else if(ch == '#' || ch == ';') {
state = state_comment;
} else if(ch == '[') {
pos = 0;
state = state_section;
memset(§ion, 0, sizeof(section));
} else if(isalnum(ch)) {
pos = 0;
state = state_key;
memset(&key, 0, sizeof(key));
memset(&value, 0, sizeof(value));
if(!(flags & INI_PARSER_FLAG_DONT_LOWERCASE_SECTIONS_AND_KEYS)) ch = tolower(ch);
key[pos++] = ch;
}
break;
case state_comment:
if(ch == '\n' || ch == '\r') {
line += ch == '\n' ? 1 : 0;
state = state_init;
}
break;
case state_section:
if(ch == '\n' || ch == '\r') {
line += ch == '\n' ? 1 : 0;
error = INI_PARSER_ERROR_MISSING_SECTION_CLOSE_BRACKET;
} else if(ch == ']') {
if(!(flags & INI_PARSER_FLAG_DONT_TRIM_SECTIONS_AND_KEYS)) {
while(--pos >= 0 && isspace(section[pos])) section[pos] = '\0';
}
state = state_comment;
} else if(pos >= sizeof(section)-1) {
error = INI_PARSER_ERROR_SECTION_TOO_LONG;
} else {
if(!(flags & INI_PARSER_FLAG_DONT_LOWERCASE_SECTIONS_AND_KEYS)) ch = tolower(ch);
section[pos++] = ch;
}
break;
case state_key:
if(ch == '\n' || ch == '\r') {
line += ch == '\n' ? 1 : 0;
error = INI_PARSER_ERROR_MISSING_VALUE;
} else if(ch == '=' || ch == ':') {
if(!(flags & INI_PARSER_FLAG_DONT_TRIM_SECTIONS_AND_KEYS)) {
while(--pos >= 0 && isspace(key[pos])) key[pos] = '\0';
}
state = state_value;
pos = 0;
} else if(pos >= sizeof(key)-1) {
error = INI_PARSER_ERROR_KEY_TOO_LONG;
} else {
if(!(flags & INI_PARSER_FLAG_DONT_LOWERCASE_SECTIONS_AND_KEYS)) ch = tolower(ch);
key[pos++] = ch;
}
break;
case state_value:
if(ch == '\n' || ch == '\r' || ch == ';' || ch == '#') {
line += ch == '\n' ? 1 : 0;
if(!(flags & INI_PARSER_FLAG_DONT_TRIM_VALUES)) {
while(--pos >= 0 && isspace(value[pos])) value[pos] = '\0';
}
state = (ch == ';' || ch == '#') ? state_comment : state_init;
error = callback(section, key, value, callack_data);
} else if(pos == 0 && isspace(ch)) {
continue;
} else if(pos >= sizeof(value)-1) {
error = INI_PARSER_ERROR_VALUE_TOO_LONG;
} else {
value[pos++] = ch;
}
break;
default:
error = INI_PARSER_ERROR_INVALID_PARSER_STATE;
}
}
if(state == state_value && !error) {
if(!(flags & INI_PARSER_FLAG_DONT_TRIM_VALUES)) {
while(--pos >= 0 && isspace(value[pos])) value[pos] = '\0';
}
error = callback(section, key, value, callack_data);
}
if(!(flags & INI_PARSER_FLAG_PARSE_STRING)) {
fclose(p_file);
}
return error;
}
Beispielprogramm
Example program
#include "ini_parse.h"
#include <string>
#include <iostream>
using namespace std;
/**
* Global variable, used to show the purpose of the void*
* parameter of the callback. Of cause we could set it
* directly in the callback as it is global, but - who
* cares :)
*/
unsigned int example_num_of_values = 0;
/**
* The callback - here you set your config dependent on which
* section and key you get.
*
* @param const char *section
* @param const char *key
* @param const char *value
* @param void *data
* @return int
*/
int ini_parse_example_callback(const char *section, const char *key,
const char *value, void *data)
{
// Let's increment the data value to do something with this pointer.
(*((unsigned int*)data))++;
// ... and output what we got here
cout << "\e[0;32m "
<< (*((unsigned int*)data)) << ")"
<< "\e[0;35m "
<< "[" << section << "]"
<< "\e[0;34m "
<< key
<< "\e[0m"
<< " = " << value
<< endl
;
// We can return an error code != 0 as well, the parser will exit then
// immediately with this error code.
return 0;
}
/**
* Process main - invokes the ini parser
*
* @param int argc
* @param char **argv
* @return int
*/
int main(int argc, char **argv)
{
if(argc < 2 || argv[1] == NULL) {
cerr << "\e[0,31mNo file ini given (first and only command line argument)\e[0m" << endl;
return -1;
}
int error = ini_parse(
argv[1], // The file name
ini_parse_example_callback, // The callback
&example_num_of_values, // Pointer to some data we like to work with
0 // FLAGS (see below)
// Instead of parsing a file, you can set this flag and set the first
// argument of the function to a string (const char*) containing the text
// in ini file format.
// | INI_PARSER_FLAG_PARSE_STRING
// This would prevent to automatically send lowercase sections and keys
// to the callback
// | INI_PARSER_FLAG_DONT_LOWERCASE_SECTIONS_AND_KEYS
// This would prevent to remove leading and tailing whitespaces from sections
// and keys
// | INI_PARSER_FLAG_DONT_TRIM_SECTIONS_AND_KEYS
// This would prevent to remove leading and tailing whitespaces from values
// | INI_PARSER_FLAG_DONT_TRIM_VALUES
// This would prevent to remove the UTF-8 BOM (three bytes) at the beginning
// of the file.
// | INI_PARSER_FLAG_DONT_ALLOW_BOM
// Normally comments (everything after ";" or "#") are removed from values,
// if you have values that may contain these characters you can disable
// this feature.
// | INI_PARSER_FLAG_DONT_ALLOW_COMMENTS_AFTER_VALUES
);
if(error) {
cerr << endl << "\e[0;31mIni error: " << ini_parse_err_str(error) << "\e[0m" << endl;
} else {
cerr << endl << "\e[0;32mExit without any errors.\e[0m" << endl;
}
return error;
}
Makefile
CC=g++
CFLAGS=-c -Wall
all: ini-parse clean
clean:
@rm -f *.o >/dev/null 2>&1
run: all
@echo "\n--------- run ---------"
@echo "exit code =" $(shell ./ini-parse example.ini >example.txt 2>&1; echo $$?) "\n"
@cat example.txt
@rm -f ini-parse >/dev/null 2>&1
ini-parse: ini_parse.o example.o
$(CC) ini_parse.o example.o -o ini-parse
ini_parse.o: ini_parse.c
$(CC) $(CFLAGS) ini_parse.c
example.o: example.cc
$(CC) $(CFLAGS) example.cc
Beispiel-Inidatei
Example ini file
# Comment
; Other Comment
[Section 1]
a-number = 0.1
a_number = 0.2
a number = 0.3 ; "inline" comment
a.string = hello there ; This is not in the value
[SECTION 2]
KEY=value ; All of these identical keys
KEY = VALUE ; will be parsed. No error is
KEY =Value ; raised by the parser.
[INVALID SECTION MISSING BRACKET
;
; The parser will exit on parse errors
; and return a corresponding error code.
Key ; this will be an error, too
; Other errors are e.g. too long
; section names, key names or values.
Beispiel-Ausgabe
Example program output
1) [section 1] a-number = 0.1
2) [section 1] a_number = 0.2
3) [section 1] a number = 0.3
4) [section 1] a.string = hello there
5) [section 2] key = value
6) [section 2] key = VALUE
7) [section 2] key = Value
Ini error: Missing ini section closing bracket