A deceptively simple way to add a configuration files to an existing command-line application.
- Define a
configuration file property -> command-line optionmap - Include a single C file
- Done.
The grammar is defined at runtime, so that application plugins can register whatever they want in addition to a base set of keywords.
SimpleConf is the configuration file parser used in pure-ftpd.
It is a trivial piece of code, but it is very generic and can easily be reused. Hence this standalone-version, in case it could be useful to other projects.
The code below uses getopt_long() to parse command-line arguments, but SimpleConf
is compatible with pretty much anything, as it always builds a new arguments list.
#include <getopt.h>
#include <stdio.h>
static struct option getopt_long_options[] = {
{ "name", 1, NULL, 'n' }, { "bell", 0, NULL, 'b' }, { NULL, 0, NULL, 0 }
};
static const char *getopt_options = "n:b";
void parse_options(int argc, char *argv[])
{
int flag, index = 0;
while ((flag = getopt_long(argc, argv, getopt_options,
getopt_long_options, &index)) != -1) {
switch (flag) {
case 'n': printf("Hello %s!\n", optarg); break;
case 'b': putchar(7); break;
}
}
}
int main(int argc, char *argv[])
{
parse_options(argc, argv);
return 0;
}Simple changes to the main() function in order to implement support for
configuration files:
#include "simpleconf.h"
int main(int argc, char *argv[])
{
static const SimpleConfEntry sc_defs[] = {
{ "Name (<any*>)", "--name=$0" },
{ "Bell? <bool>", "--bell"}
};
sc_build_command_line_from_file("hello.conf", NULL, sc_defs, 2,
argv[0], &argc, &argv);
parse_options(argc, argv);
return 0;
}Done.
Our hello application can now load a hello.conf configuration file such as
the following:
#################################################
## ##
## Sample configuration file for Hello ##
## ##
#################################################
# Change this to your name
Name Johnny Doe
# Change to "yes" for a (visual) bell effect
Bell no
The parser is quite tolerant and the following configuration files would work equally well:
name = Johnny Doe
bell = off
name: Johnny Doe
bell: false
The grammar is defined by a vector of SimpleConfEntry values. Each value maps
a property name and a pattern to a command-line option.
A pattern can be made of arbitrary characters, including white spaces:
{ "PreferredFruit banana", "--banana" },
{ "PreferredFruit kiwi fruit", "--kiwi" }But once again, the parser is very tolerant, and will gladly accept any number of
spaces (white spaces and/or tabs) between kiwi and fruit in the configuration
file.
Note that the same property name can appear multiple times: the first one that fully matches the given pattern is the one that will be translated to a command-line option.
Patterns can include character classes:
<alpha>: matches one or more alphabetic characters<alnum>: matches one of more alphanumberic characters<digits>: matches one or more digits<xdigits>: matches one or more hex digits<nospace>: matches one or more characters that are not whitespaces; the string can optionally be quoted to allow delimiters.<any>: matches everything except whitespaces, as well as quoted strings that can include whitespaces and delimiters. Given thekiwi fruitinput, this would only matchkiwi. Given the"kiwi fruit"input, this would matchkiwi fruit.<any*>: matches everything until the end of the line, including whitespaces, without requiring quotes.<bool>: matchesyes,on,true,1,no,off,falseand0.
The command-line translation of a config file pattern can copy parts or all of the matching input:
{ "PreferredFruit (<alpha>)", "--fruit=$0" }Capture groups are delimited by round brackets, and can include anything, although they are mostly useful with character classes:
{ "WidthAndHeight (<digits>x<digits>)", "--size=$0" }Up to 10 capture groups can be present in a pattern. The first one can be
referred to as $0, the second one as $1 and so on until $9.
{ "Size width:(<digits>) height:(<digits>)", "--size=$0x$1" }The whole input can also be referred to as $*.
The addition of a command-line switch can depend on the value of a boolean
condition. In order to do so, the property name should be suffixed with the
? character.
{ "EnableCrazyCoolFeature? <bool>", "--crazy-cool-feature" }In this example, the --crazy-cool-feature switch will only be added if the
value of the EnableCrazyCoolFeature property is either yes, on, true
or 1.
int sc_build_command_line_from_file(const char *file_name,
const SimpleConfConfig *config,
const SimpleConfEntry entries[],
size_t entries_count, char *app_name,
int *argc_p, char ***argv_p);Builds a new list of command-line arguments by parsing the configuration file
file_name according to the list of patterns entries of size entries_count,
and puts the result into argv_p and its size into argc_p.
app_name is put into (*argv_p)[0], and can be the original value for argv[0].
config can be NULL, or set to a SimpleConfConfig when using special handlers
(see below).
void sc_argv_free(int argc, char *argv[]);Deallocates the memory allocated by sc_build_command_line_from_file().
Specific keywords can cause a hook being called instead of the default behavior.
This can be useful to handle options in the configuration file that have no command-line equivalent, as well as to support recursive configuration files.
Property names starting with a ! character trigger that behavior:
{ "!Include <any*>", "$*" }In this example, instead of extending the list of command-line options, the
Include keyword will cause a special handler to be called.
SimpleConfSpecialHandlerResult special_handler(void **output_p, const char *arg, void *user_data)
{
// ... do some custom processing using the value `arg` ...
return SC_SPECIAL_HANDLER_RESULT_NEXT;
}Possible return values are:
SC_SPECIAL_HANDLER_RESULT_NEXT: keep parsing the current fileSC_SPECIAL_HANDLER_RESULT_ERROR: return an error and stop parsing the current fileSC_SPECIAL_HANDLER_RESULT_INCLUDE: parse the content of the file whose name was stored in*output_p, and continue parsing the current file afterwards.
SC_SPECIAL_HANDLER_RESULT_INCLUDE is useful to allow users to recursively
include configuration files in configuration files. A handler can sanitize
file names, turn relative file names into absolute ones, and enforce rules to
only allow specific files.
The bare minimum is to return a copy of the original file name:
SimpleConfSpecialHandlerResult special_handler(void **output_p, const char *arg, void *user_data)
{
if ((*output_p = strdup(arg)) == NULL) {
return SC_SPECIAL_HANDLER_RESULT_ERROR;
}
return SC_SPECIAL_HANDLER_RESULT_INCLUDE;
}The handler should be specified via a SimpleConfConfig value when calling
sc_build_command_line_from_file(). The user_data field can store an
arbitrary pointer, that will be conveniently available in the handler.