The main question is: do you want your configuration file to be in some Turing complete language (like Python is)? If you do want that, you might also consider embedding some other (Turing complete) scripting language like Guile or Lua (because there could be perceived as "simpler" to use, or to embed, than Python is; read the chapter on Extending & Embedding Python). I won't discuss that further (because other answers -e.g. by Amon- discussed that in depth) but notice that embedding a scripting language in your application is a major architectural choice, that you should consider very early; I really don't recommend making that choice later!
A well known example of a program configurable thru "scripts" is the GNU emacs editor (or probably AutoCAD in the proprietary realm); so be aware that if you accept scripting, some user would eventually use -and perhaps abuse, in your point of view- that facility extensively and make a multi-thousand lines script; hence the choice of a good enough scripting language is important.
However (at least on POSIX systems), you might consider convenient to enable the configuration "file" to be dynamically computed at initialization time (of course, leaving the burden of a sane configuration to your system admin or user; actually it is a configuration text which comes from some file or from some command). For that, you could simply adopt the convention (and document it) that a configuration file path starting with e.g. a !
or a |
is actually a shell command that you would read as a pipeline. This leaves your user with the choice of using whatever "preprocessor" or "scripting language" he is the most familiar with.
(you need to trust your user about security issues if you accept a dynamically computed configuration)
So in your initialization code, your main
would (for example) accept some --config
argument confarg
and get some FILE*configf;
from it. If that argument starts with !
(i.e. if (confarg[0]=='!')
....), you would use configf = popen(confarg+1, "r");
and close that pipe with pclose(configf);
. Otherwise you would use configf=fopen(confarg, "r");
and close that file with fclose(configf);
(don't forget the error checking). See pipe(7), popen(3), fopen(3). For an application coded in Python read about os.popen, etc...
(document also for the weird user wanting to pass a configuration file named !foo.config
to pass ./!foo.config
to bypass the popen
trick above)
BTW, such a trick is only a convenience (to avoid requiring the advanced user to e.g. code some shell script to generate a configuration file). If the user want to report any bug, he should send you the generated configuration file...
Notice that you could also design your application with the ability to use and load plugins at initialization time, e.g. with dlopen(3) (and you need to trust your user about that plugin). Again, this is a very important architectural decision (and you need to define and provide some rather stable API and convention about these plugins and your application).
For an application coded in a scripting language like Python you could also accept some program argument for eval or exec or similar primitives. Again, the security issues are then the concern of the (advanced) user.
Regarding the textual format for your configuration file (be it generated or not), I believe that you mostly need to document it well (and the choice of some particular format is not that important; however I recommend to let your user be able to put some -skipped- comments inside it). You could use JSON (preferably with some JSON parser accepting and skipping comments with usual //
till eol or /*
...*/
...), or YAML, or XML, or INI or your own thing. Parsing a configuration file is reasonably easy (and you'll find many libraries related to that task).