class ref name: progconf
class name: progconf_o
category-group: quickdirt
layer: 11
header file: z_progconf.h

synopsis.
The "program configuration" class (progconf_o) provides a new level of comfort and ease in configuring program, thread, or object variables. In other words, you can package up all the parameters for your program into an initialization (boot, or "ini") file, and by following the rules for definining what the program's variables are, load up the values in the boot file into a single object - progconf_o, which you can query throughout your program's code to obtain parameter values. In addition, it will parse and obtain values from the command line, and incorporate these values into the same object- whether or not the parameter is in the INI (boot) file.

description.
the program configuration object allows for setting variables in 3 ways:

  • INI file
  • command-line
  • calling "config()"

The variables, and their values, are written to a list databag. The biggest task in using this class is the initial set-up. One needs to create an array of parameters, like so:

#define MY_USEDB         0
#define MY_STOMPDB       1
#define MY_DBSERV        2
#define MY_DB_ACCT       3
#define MY_DB_PASS       4
#define MY_RUN_ISWET     5
#define MY_RUN_NTHREADS  6
#define MY_ABORT_ONERR   7

struct progconf_param myglo_params[] = { { MY_USEDB, 1, "database/use_db", "useDB", "useDB", "YES" }, { MY_STOMPDB, 0, "database/stomp_db", "F", "stomptables", "" }, { MY_DBSERV, 1, "database/server", "server", "dbserver", "localhost" }, { MY_DB_ACCT, 1, "database/account", "user", "dbuser", "YES" }, { MY_DB_PASS, 1, "database/password", "pass", "dbpassword", "" }, { MY_RUN_ISWET, 0, "run/wet", "is_wet", "wet", "YES" }, { MY_RUN_NTHREADS, 2, "run/numthreads", "nthreads", "numthreads", "1" }, { MY_ABORT_ONERR, 0, "run/abort_data", "abort", "abort", "" }, { -1, 0, NULL, NULL, NULL, NULL },

The meaning of the fields is as follows: field 1: a unique numeric identifier for the parameter. It is your responsibility to make sure they are unique.
field 2: a bit-flag for command-line processing, that provides some info about the parameter: 1 - it is purely a flag. That is, it takes no arguments. 2 - it takes an argument. 6 - it takes an argument, which must be an integer (this is the concatenation of {bit 1} + {bit 2}, or 2+4=6).
field 3: the name used in the INI file. the string in this field must be the "full path", eg, group name, followed by item name; separated by a slash ('/').
field 4: the command-line argument keyword. this does not include the preceding '-' (or '/', if the object is so configured).
field 5: the keyword argument for the "config()" member function.
field 6: the default value (this must be represented by a string)

The object is initialized with this array, like so:
    main ()
    {
        progconf_o ppx0(myglo_params);
        progconf_o ppx1(myglo_params, 8);
        progconf_o ppx2(myglo_params, 4);
        progconf_o ppx3();
        ppx3.load (myglo_params);
    }
In the first case-instance ("ppx0"), the last row of data must have a -1 in the ID field (or a null in any char * field will do the same trick). This is the recommended approach.

In the second case-instance ("ppx1"), you provide a count of how many rows there are. Any rows with ID-field equal to -1 will be ignored. Slightly more dangerous.

The third case-instance ("ppx2") is really the same as the second, but you can specify loading less rows than there actually are (for whatever reason).

In the 4th case, the object ("ppx3") is empty due to its default construction. The object is loaded with parameters via an explicit call of the load() member function. You may have guessed, this function works in the same way as the constructor - you can give it an optional count number as the second argument. If this argument is 0 (the default, as in this case), row processing will terminate when ID = -1. There is a [hidden-optional] third argument, a TRUE / FALSE flag that tells whether to add to vs. replace the existing data (if any).

Once the object's parameters have been defined, you can set their values in any of 3 ways:

  • load_ini() - this loads the parameters from an INI file. The INI file must have been specified earlier via set_ini(), or there is a variant of load_ini() that takes a file_o instance as the argument, for on-the-fly loading.
  • cmdline(argc, argv) - this will forward the command-line to the object. Of course, the parameters could be fake, eg, not really from a command-line.
  • config() - you can call this to set a parameter (or several). This function takes a paramstring_o variable as its argument, which can hold 1 or more parameters (actually, 0 or more). Thus you can mix and match in many variations here - call this repeatedly with 1 parameter, or bunch them up and call this once.
The first 2 functions call the last one - everything goes through config(). You can mix and match all 3. Successive calls will over-write existing variables and add new ones.

There is a danger of forgetting to load the object with an array of 'progconf_param' record structures. If that is the case, load_ini() will fail. Beware.

warning #2: make sure your 'struct progconf_param' array ends with this set:

  { -1, 0, NULL, NULL, NULL, NULL }
You can exclude this last row if you know exactly how many elements are in your array and pass it to the progconf_o constructor:
struct progconf_param my_params[] =
{
  { MY_USEDB,   1, "database/use_db", "useDB",  "useDB",      "YES" },
  { MY_DBSERV,  1, "database/serv", "server", "dbserv", "localhost" },
  { MY_DB_ACCT, 1, "database/account",  "user", "dbuser",     "YES" },
  { MY_DB_PASS, 1, "database/password", "pass", "dbpass",     "" }
};
class X
{
private:
  progconf_o my_conf;
};
X::X() : my_conf(my_params, 4) { /* ... */ }
Such a static configuration is not recommended as the array ('my_params[]') typically changes over time and syncing the count (4, in this example) with the constructor will probably be forgotten. Easier and safer to use the terminator-sentinel row.

Once any parameter has been modified, a flag keeps track of the fact that the object has been modified, but there is no way within the object to revert to a parameter's old value (this functionality might be added in the future). If the object has changed (at least one item), it can be written back to the INI file. You can unconditionally set the "is-modified" flag on or off.

You can embed environment variables in the INI parameter values by wrapping the variable name with "${" .. "}". If a matching environment variable is found, the string will be replaced. Suppose you have an environment variable "MY_DIR". You can use it like so:

[mysection]
varpath = ${MY_DIR}\program\parameters
If there is no environment variable matching "MY_DIR", no replacement will be done.

A new feature (2017) is that you can check the values of each parameter in an INI once it is loaded. The check can be done inside load_cmdline_wdefault_INI(), so that you can validate the values at boot load time. However, the application must supply a subroutine that does the checking as progconf_o has no way of knowing what values are legal for a given variable. There are endless possibilities:

  • checking that a value is an integer (call zis_allnumbers(val));
  • checking that a value is all letters (use z_is_str_allletters());
  • checking that a value is like a "word-name" (ie, follows the rules of naming c/c++ variables). Use is_z_valid_variable_name())
  • checking that a value is a "yes" or "no" type value (call zis_yesno() and examine the error parameter);
  • checking that a value is a "yes/on" or "no/off" type value (call zis_yeson() instead zis_yesno(), and examine the error parameter);
  • checking that a value is one of a set of keywords. This will entail writing long if statements ("if (s == "THIS" || s == "THAT" .. ");
  • checking that a value is a real number - similar to the "yes/no" checks, but using the subroutine z_str_to_double());
This list is just a sample of the possibilities. In addition to syntax checking, a parameter might be limited to certain values. A day-of-month value would in addition to checking if it's an integer, checking the integer value to be within [1..31]. Or maybe it must be a non-negative value. A lot of possibilities.

The way to use this is to first make a subroutine or function that checks if the value of a given key/value pair is correct. Its signiture is like so:

boolean my_validator (const string_o &key, const string_o &val, int *pie);
Its internals will probably be similar to this:
{ int i, ie0, ie1; boolean reply = FALSE; *pie = 0; - if (kw == "database/stomp_db") { zis_yesno(val, FALSE, &ie0); reply = (!ie0) ? TRUE : FALSE; } else if (kw == "database/connect") { reply = (val == "ODBC" || val == "ADO") ? TRUE : FALSE; } else if (kw == "run/allow_errors") { zis_yesno(val, FALSE, &ie0); reply = (!ie0) ? TRUE : FALSE; } else { *pie = zErr_Param_BadVal; return FALSE; } - return reply; }
Next, simply call set_validation() before calling load_cmdline_wdefault_INI():
int ie1, ie0 = my_params.set_validation(my_validator); ie0 = my_params.load_cmdline_wdefault_INI (my_argc, my_argv, TRUE, &ie1);
the "load_cmdline.." call will return -1 if one or more bad parameter values was found; the error code (in 'ie1', above) will be set to 'zErr_DirtyData'. You can get a count of how may parameters were found to have invalid values by calling howmany_badparams() afterwards. You can't find out which parameter(s) is invalid at this point, but you can use isvalid_paramvalue() on any given parameter name (it must include the group name. the format is "group/name"):
int ie0, ie1; string_o skey = "run/allow_errors"; string_o sval = my_params.getvalue_byID(8, &ie1); boolean isok = my_params.isvalid_paramvalue(skey, sval);
Actually, using isvalid_paramvalue() this way is rather pointless since you can just as well call your validation function (my_validator(), in our example) directly. Getting the value takes a little extra work. You need to find the parameter keyword's ID in your progconf_param[] array. Here, we knew the parameter had an ID value of 8. But you can shorten this process by using isvalid_paramvalue() with a different, value-less signiture:
string_o skey = "run/allow_errors"; boolean isok = isvalid_paramvalue(skey);
Now progconf_o will do the work of cycling thru the list of parameters to find the value of 'skey'.

member functions (primary)

progconf_o_o()
SIGNATURE: progconf_o ()
SYNOPSIS: creates an empty progconf object. The object has no internal parameters.
 

progconf_o_o(<args>)
SIGNATURE: progconf_o (const struct progconf_param *apar)
SYNOPSIS:
creates a progconf object. The object's internal parameters are loaded by the contents in 'apar'. This must be a pointer to an array of "progconf param" records (eg, "struct"). The last entry must have the value apar[LAST].id == -1. It is the application (read: your) responsibility to ensure that this array is properly terminated.
 

progconf_o_o(<args>)
SIGNATURE: progconf_o (const struct progconf_param *apar, count_t n)
SYNOPSIS:
creates a progconf object. The object's internal parameters are loaded by the contents in 'apar'. This must be a pointer to an array of "progconf_param" records (eg, "struct"). Up to 'n' records will be read and processed. Any records with value apar[LAST].id == -1 will be skipped. It is your responsibility to ensure that the value of 'n' is correct.
 

destructor
SIGNATURE: ~progconf_o ()
SYNOPSIS: The destructor. all data inside is wiped out.
 

is_loaded()
SIGNATURE: boolean is_loaded () const
SYNOPSIS: tells if the object has data from an INI file loaded into it.
TRAITS: this function is inline
 

is_modified()
SIGNATURE: boolean is_modified () const
SYNOPSIS:
tells if the object has any data that has changed in value. If any one or more parameters have been changed, the object is considered to be modified.
TRAITS: this function is inline
 

am_complaining()
SIGNATURE: boolean am_complaining () const
SYNOPSIS: tells if the 'loud_mode' flag has been set (this is done via "set_complain()").
RETURNS:
TRUE: 'loud_mode' flag is ON
FALSE: 'loud_mode' flag is OFF
 

cmdkw_byID()
SIGNATURE: string_o cmdkw_byID (const int x, boolean dash = FALSE, int *pi = NULL) const
SYNOPSIS: get the "" matching the index value 'x'
DESCRIPTION:
this searches for the ID field 'x' (using getvalue_byID) and returns the 'console_name' string keyword value. it is a convenience function, mapping ID to console_name. You can easily build it from other progconf_o member functions.
 

confname_byID()
SIGNATURE: string_o confname_byID (const int x, int *pi = NULL) const
SYNOPSIS: get the "" matching the index value 'x'
DESCRIPTION:
this searches for the ID field 'x' (using getvalue_byID) and returns the 'config_name' value. it is a convenience function, mapping ID to this field.
 

getvalue_byID()
SIGNATURE: string_o getvalue_byID (const int x, int *pi)
SYNOPSIS: get a parameter's value
PARAMETERS

  • x: ID value of the parameter to retrieve
  • pi: error indicator [output] variable. values:
    0: all ok, success, value retrieved
    1: item not found!
    2: input parameter invalid (out of bounds)
    9: data bag inconsistency (internal error)
DESCRIPTION:
this searches the internal list databag for the item identified by 'x', which is the ID field of the item specified in the "struct progconf_param *" array struture, provided at object construction time (or via "load()").
 

findarg_inparamset()
SIGNATURE: int findarg_inparamset (const string_o &s, count_t &idx)
SYNOPSIS:
This looks up the item according to the command-line argument name. It actually returns 2 parameters: the ID of the item whose console name (eg "command-line argument") matches, and the position in the 'par_info' array of parameters. Which to use is up to you.

PARAMETERS

  • s: [input] a string object containing the 'console name' of the desired parameter.
  • idx: [output] a number which is the position in the list of parameters.
RETURNS:
[n >= 0] the ID number of the parameter
-1: not found (no match)
 

find_param()
SIGNATURE: const progparam_o &find_param (const int code, const string_o &s, int *pi)
SYNOPSIS:
this is a swiss-army-knife type function to look up a parameter according to several different search criteria (actually all of them use an exact-match search). which is invoked depends on the 'code' value:

0: use 'ID' (provided as a string)
1: use 'name'
2: use 'config_name'
3: use 'console_name'
4: use position (provided as a string)

PARAMETERS
  • code: [input] an enumeration that tells the function which field in "struct progconf_param" to use to compare with input parameter 's' (see description, above).
  • s: a string containing the search criteria value to look for in the list of parameters. Note that for integral field types (code == 0 or 1), the contents of this string will be converted to an int.
  • pi: error indicator [output] variable. values:
    0: success-ok, item retrieved and handle returned
    zErr_Illegal_CaseValue: illegal 'code' value
    zErr_Param_NotFound: item not found in set
RETURNS:
[a reference to object in the list]
progconf_o::bad_reference(): the item was not found. use the output variable 'pi' to determine which of the 2 return values is returned.
 

set_separator()
SIGNATURE: int set_separator (const char c, int *pi = NULL)
SYNOPSIS:
Allows the user to change the default separator character for path strings used to reference items in an INI file. used to separate group from item. This character is '/' by default.
PARAMETERS

  • c: the character to use to separate words (names) in an INI file path. This cannot be a character, number, or '_'.
  • pi: an [output] error indicator variable. values:
    0: item successfully changed
    1: input parameter 'c' is a character (not allowed)
    2: input parameter 'c' is a number (not allowed)
RETURNS:
0: success
-1: error (see value of 'pi')
 

force_modified()
SIGNATURE: int force_modified (boolean is_on)
SYNOPSIS:
turns on (or off, if 'is_on' is FALSE) the "is-modified" flag ('my_ismodified'), unconditionally. This overrides any setting done internally. Usage of this function should be done by experts only. Use with caution.
An alias, clear_modified(), is provided, which is identical to force_modified(FALSE).
RETURNS: 0 (always)
 

clear_modified()
SIGNATURE: int clear_modified ()
SYNOPSIS: equivalent to force_modified(FALSE).
RETURNS: 0 (always)
 

set_complain()
SIGNATURE: void set_complain (boolean b = TRUE)
SYNOPSIS: turns on (or off, if b is FALSE) emitting error messages to the console window (eg, stdout)
 

set_ini()
SIGNATURE: int set_ini (const file_o &f, boolean use_incmd = FALSE, int *pi)
SYNOPSIS:
defines the INI file, eg, tells the object where the INI file is, for subsequent loading via "load_ini()". If 'use_incmd' is set to TRUE, member function 'cmdline()' will act as if it had a "-ini" keyword in its list, whether or not there is one. If there is one, that value over-rides the path set here. If absent from the actual argv[] list, the name specified to this function will be used.
By default, this function only sets the name ('use_incmd' == FALSE).
PARAMETERS

  • f: [input] a file object containing the path and file name set. if the path is not set, the current directory will be used. the file name part of the file object must be set.
  • use_incmd: if TRUE, not only will the INI file name be set, but "cmdline()" will assume this file ('f') is to be loaded, if no "-ini" argument is encountered.
  • pi: an [output] error indicator variable. values:
    0: success
    1: name not set (ERROR)
    2: path not set (WARNING ONLY)
    3: name (and possibly) path set, but file not found (WARNING ONLY)
RETURNS:
0: success
-1: error, probably file name not set (see value of 'pi')
 

load_ini()
SIGNATURE: int load_ini (const file_o &f, int *pi)
SYNOPSIS:
loads the INI file specified by 'f' into the object. This function is basically a convenience, combining "set_ini()" and "load_ini()". Note that processing is not halted if a single item (or multiple items) are read that cannot be processed. Hence, for full error handling, the output variable 'pi' must be inspected after callling this function.
PARAMETERS

  • pe: an [output] error indicator variable. values:
    0: success; name retrieved
    1: name not set
    2: null parameter provided
  • pi: an [output] error indicator variable. values:
    0: success
    zErr_Resource_Missing: file not found
    2: error in INI load
    zErr_NotInitialized: "struct progconf_param" set has not been loaded
    zErr_DataBadSyntax: 1 or more items in INI file are unknown. Note, processing is not halted upon hitting a mangled item found in the file.
RETURNS:
0: success
-1: error (see value of 'pi')
 

load_ini()
SIGNATURE: int load_ini (int *pi)
SYNOPSIS: loads an INI file. The file must have been specified by a previous call to "set_ini()".
PARAMETERS

  • pe: an [output] error indicator variable. values:
    0: success; loaded
    1: file not found
    2: error in processing
  • RETURNS:
    0: success
    -1: error (see value of 'pi')
     

    load_cmdline_wdefault_INI()
    SIGNATURE: int load_cmdline_wdefault_INI (const int argc, const char **argv, const boolean use_env = FALSE, int *pi = NULL)
    SYNOPSIS:
    this is very much like and related to cmdline(), but does not require "-ini" [filename] in the command-line argument list. By calling this routine instead, a default INI file will be searched for, in the same directory as the program and with the same base name as the program.
    For example, if the program is called "foobar.exe", an implicit command-line argument pair "-ini" foobar.ini" will be constructed. If that does not exists, and 'use_env' is TRUE, any INI file specified by INI_FILENAME will be used. Thus, if there is an environment variable INI_FILENAME whose value is "zulufile.rc", it will be as if member function cmdline() has been executed with the arguments "-ini zulufile.rc". In all cases a file in the current directory with name pattern "[basename].ini" is the first INI file to be checked for.
    If there is an INI file in the given command-line argument list, this subroutine will use that. INI files are spec ified by preceding the file name with a special "INI" keyword, which is [hard-coded / fixed] to be one of the following:

    -ini
    --ini
    /ini
    ini
    
    Case is irrelevant. Thus, "-ini" and "-INI" are equivalent in this routine. Keywords such as "ini", "--ini", and "/ini" are included also, as some environments (eg MFC or inside a debugger might strip away a leading "-" or convert "--ini" to "/ini".
    PARAMETERS
    • argc: a count of the number of elements in 'argv', which is a pointer to an array of char * "string" character buffers.
    • argv: an array of "char *" pointers. The first pointer (argv[0]) is ignored by this function [always].
    • use_env: a control flag. If TRUE, a search will be done for environment variable INI_FILENAME. If there is such a variable in the local environment, its value will be used for the [entire] file name of the INI file. This is done after a cursory check for a local INI file in the same directory as the program executable file with the same basename as the binary binary. If such a file exists, it will be used as the program's INI file. If FALSE, this attempt will be skipped.
    • pi: an [output] error indicator variable. possible values:
      0: success; INI file successfully loaded
      zErr_Param_Incomplete: INI keyword ("ini", "-ini", "--ini", or "/ini") found, but no argument follows (ie, keyword is at the end of the argument list)
      zErr_TooMuchData: too many arguments at the argument list. Maximum of 1,024 keywords are permitted.
      zErr_Impossible_Value: "-ini" not found in the list of command-line arguments
      zErr_Data_Inconsistent: number of arguments in the command-line arguments list does not match the claimed number of arguments ("argc")
    RETURNS:
    0: success
    1: INI file not found
    -1: error (see value of 'pi')
     

    cmdline()
    SIGNATURE: int cmdline (int argc, char **argv, int *pi)
    SYNOPSIS:
    loads the object's parameters from the values found in 'argv'. The input parameters argc and argv are intended to come from the program's command-line (though not mandatory).
    PARAMETERS

    • argc: a count of the number of elements in 'argv', which is a pointer to an array of char * "string" character buffers.
    • argv: an array of char * (eg, string) pointers. The first pointer (argv[0]) is ignored by this function [always].
    • pi: an [output] error indicator variable. possible values:
      0: success; object loaded; everything on the command-line parsed ok
      zErr_Data_Unknown: illegal (unknown) keyword was encountered
      zErr_Data_BadFormat: keyword found without a proper prefix (ie, '-')
      zErr_Param_NotFound: "-ini", no file name argument was provided (missing)
      zErr_Read_Failed: the INI file failed to load
      zErr_Data_Incomplete: the command-line parameter requires a missing argument
      (the remaining error codes come from 'progconf_o::config()', which is called internally)
      zErr_Param_NotFound: an item was not found
      zErr_Data_BadFormat: [internal error] a databag found, but its type was not "simple" (panic; should not happen)
    RETURNS:
    0: success
    1: INI file not specified in command-line parameter list
    -1: error (see value of 'pi')
     

    config()
    SIGNATURE: int config (paramstring_o &ps, int *pi)
    SYNOPSIS:
    sets the internal parameter values based on the contents of 'ps'. The paramstring variable can have multiple sets of name-value pairs, so multiple (0 or more) parameter values can be set.
    This function is similar in setvalue_byID(), but can do a batch of parameters at once, instead of one per function call. Also, the command-line keystring identifyier name is used, not the parameter's ID value.
    PARAMETERS

    • ps: a string containing a whitespace-separated list of name-value pairs to set.
    • pi: an [output] error indicator variable. values:
      0: success; 'ps' processed, item(s) loaded
      zErr_Param_NotFound: an item was not found
      zErr_Data_BadFormat: [internal error] a databag found, but its type was not "simple" (panic; should not happen)
    RETURNS:
    0: success
    -1: error (see value of 'pi')
     

    load()
    SIGNATURE: int load (const progconf_param *apar, count_t n = 0, boolean stomp = TRUE, int *pi = NULL)
    SYNOPSIS: loads an array of progconf_param structure records into the object.
    PARAMETERS

    • apar: an array of progconf_param records (see main discussion on this page for more details)
    • n: number of items in array 'apar'. Could be less than the actual number.
    • stomp: if TRUE, previous contents are deleted. if FALSE, any new items are added to the existing ones.
    • pi: an [output] error indicator variable. values:
      0: successfully loaded parameters into the object
      1: load() already done earlier
    RETURNS:
    0: success
    -1: error
     

    getvalue_byID()
    SIGNATURE: string_o getvalue_byID (const int x, int *pi = NULL)
    SYNOPSIS: retrieves the current value of item whose ID is 'x'.
     

    setvalue_byID()
    SIGNATURE: int setvalue_byID (const int x, const string_o &s, int *pi = NULL)
    SYNOPSIS: retrieves and sets the current value of item whose ID is 'x'. The new value is set to the contents of 's'.
     

     

    write_ini()
    SIGNATURE: int write_ini (boolean clear = TRUE, boolean force = FALSE, int *pi = NULL)
    SYNOPSIS:
    writes the current contents to its INI file. The file must have been specified earlier by calling the member function set_ini(). If there are no changes in the contents, and 'force' is FALSE, the file will not be written if it exists. If it does not exist, the file will be created and the object's data written out to it.
    PARAMETERS

    • clear: [input] a control parameter. If TRUE, the object's "is modified" flag will be cleared (set to FALSE). This is the default action.
    • force: [input] a control parameter. If TRUE, the file will be written even if no INI file was loaded earlier and no changes occurred in the data.
    • pi: [output] error indicator variable. values:
      0: successfully written
      zErr_OperationForbidden: the object was not modified, and 'force' was not set.
      zErr_PriorOpNotDone: the object was not loaded from an INI file, and 'force' was not set.
     

    write_ini()
    SIGNATURE: int write_ini (inifile_o &f, boolean clear = TRUE, boolean force = FALSE, int *pi = NULL)
    SYNOPSIS: writes the current contents to an INI file. Any INI file can be specifed, even if different from the original load file.
    PARAMETERS

    • f: the INI file to write to. The file address (path and file name) must have been defined prior.
    • clear: [input] a control parameter. If TRUE, the object's "is modified" flag will be cleared (set to FALSE).
    • force: [input] a control parameter. If TRUE, the file will be written even if no INI file was loaded earlier and no changes occurred in the data.
    • pi: [output] error indicator variable. values:
      0: successfully written
      zErr_OperationForbidden: the object was not modified, and 'force' was not set.
      zErr_Create_Failed: could not write to the specified file.
      zErr_Write_Failed: problem while trying to create the file.
      zErr_Resource_Unavail: failed to load the original INI file.
      zErr_Data_BadFormat: incorrect format encountered while attempting to parse the INI file contents.
      zErr_NotFound: unknown group and/or item name encountered.
      zErr_Oddness: no '=' detected on 2nd pass (should not happen)
     

    print()
    SIGNATURE: string_o print (int *pi = NULL)
    SYNOPSIS:
    formats a string representing the object. that is, the contents of the program configuration object is formatted as if it was in a bare-bones INI file. groups are separated by a single blank line. there are no embedded comments.
     

    bugs.
    at the moment (2012), load() can be done only 1 time (or more precisely, at most 1 time betweeen reset() calls). Thus, this object is currently a 1-shot deal.