// **************************************************************************
/** @class Configuration
@brief Commandline parsing, resource file parsing and database access
@section User For the user
The Configuration class will process the following steps:
Check the command-line for --default=default.rc (If no configuration
filename is given on the command-line use \e program_name.rc instead. (Note
that the name is retrieved from \b argv[0] and might change if you start
the program through a symbolic link with a different name)
Read the "database=user:password@database:port/database" entry from the file.
(For details about the syntax see Configuration::parse_database)
The retrieved entry can be overwritten by
"--database=user:passwd@server:port/database" from the command line. If
neither option is given no configuration data will be read from the
database. To suppress any database access use \b --no-database.
Check the command-line for -C priority.rc
The configuration data is now evaluated from the following sources in
the following order. Note that options from earlier source have
priority.
- (1) Commandline options
- (2) Options from the high prioroty configuration-file (given by \b -C or \b --config)
- (3) Database entries
- (4) Options from the default configuration-file (given by \b --default, defaults to \b program_name.rc)
- (5) Environment variables
Which options are accepted is defined by the program. To get a list
of all command-line option use \b --help. This also lists all other
available options to list for exmaple the options available in the
configuration files or from the databse. In addition some default options
are available which allow to debug parsing of the options, by either printing
the options retrieval or after parsing.
Options in the configuration files must be given in the form
- key = value
which is equivalent to the command-line option --key=value.
If there are sections in the configuration file like
\code
[section1]
key = value
\endcode
the key is transformed into section1.key (which would be equivalent
to --section1.key)
@attention
In principle it is possible that an exception is thrown before options
like \b --help are properly parsed and evaluated. In this case it is
necessary to first resolve the problem. Usually, this mean that there
is a design flaw in the program rather than a mistake of usage.
For more details on the order in which configuration is read,
check Configuration::Parse. For more details on the parsing itself
see the documentation of boost::program_options.
@section API For the programmer
The Configuration class heavily uses the
C++ boost library
and makes heavy use of the
boost::program_options
The databse access is based on the
MySQL++ library.
The basic idea is to have an easy to use, but powerfull setup. The setup on
all options is based on a special syntax of options_description. Here is an
example:
\code
int opt = 0;
po::options_description config("Section");
config.add_options()
("option1", po_string(), "This is option1")
("option2", po_int(22), "This is option2")
("option3,o", po_double()->required(), "This option is mandatory")
("option4", po_int(&opt), "This is option 4")
("option5", po_strings(), "A list of strings")
("option6", po_strings(), "A list of strings")
("option7", po_strings(), "A list of strings")
;
\endcode
This will setup, e.g., the commandline option '--option1 arg' (which
is identical to '--option1=arg'. Option 3 can also be expressed
in a short form as '-o arg' or '-o=arg'. Option 2 defaults
to 22 if no explicit value is given. Option 3 is mandatory and an exceptionb
is thrown if not specified. Option 4 will, apart from the usual access to the
option values, also store its value in the variable opt.
The used functions po_*() are defined in configuration.h and are abbreviations.
Generally speaking also other variable types are possible.
If the options are displayed, e.g. by \b --help the corresponding section will
by titled \e Section, also untitled sections are possible.
If an option can be given more than once then a std::vector can be used.
Abbreviations po_ints(), po_doubles() and po_strings() are available.
In addition to options introduced by a minus, so calles positional options
can be given on the command line. To describe these options use
\code
po::positional_options_description p;
p.add("option5", 2); // The first 2 positional options
p.add("option6", 3); // The next three positional options
// p.add("option7", -1); // All others, if wanted
\endcode
This assigns option-keys to the positional options (arguments) in the
command-line. Note that the validity of the given commandline is checked.
Hence, this way of defining the options makes sense.
As needed options_descriptions can be grouped together
\code
po::options_description config1("Section1");
po::options_description config2("Section2");
po::options_description configall;
configall.add(config1);
configall.add(config2);
\endcode
The member functions of Configurations allow to define for which option
source these options are valid. The member functions are:
\code
Configuration conf;
conf.AddOptionsCommandline(configall, true);
conf.AddOptionsConfigfile(config1, true);
conf.AddOptionsDatabase(config2, true);
// To enable the mapping of the position arguments call this
conf.SetArgumentPositions(p);
\endcode
If the second option is false, the options will not be displayed in any
\b --help directive, but are available to the user. Each of the functions
can be called more than once. If an option should be available from
all kind of inputs AddOptions() can be used which will call all
four other AddOptions() functions.
A special case are the options from environment variables. Since you might
want to use the same option-key for the command-line and the environment,
a mapping is needed (e.g. from \b PATH to \b --path). This mapping
can be implemented by a mapping function or by the build in mapping
and be initialized like this:
\code
conf.AddEnv("path", "PATH");
\endcode
or
\code
const string name_mapper(const string str)
{
return str=="PATH" ? "path" : "";
}
conf.SetNameMapper(name_mapper);
\endcode
Assuming all the above is done in a function calles SetupConfiguration(),
a simple program to demonstrate the power of the class could look like this:
\code
int main(int argc, char **argv)
{
int opt;
Configuration conf(argv[0]);
SetupConfiguration(conf, opt);
po::variables_map vm;
try
{
vm = conf.Parse(argc, argv);
}
catch (std::exception &e)
{
po::multiple_occurrences *MO = dynamic_cast(&e);
if (MO)
cout << "Error: " << e.what() << " of '" << MO->get_option_name() << "' option." << endl;
else
cout << "Error: " << e.what() << endl;
cout << endl;
return -1;
}
cout << "Opt1: " << conf.GetString("option1") << endl;
cout << "Opt2: " << conf.GetInt("option2") << endl;
cout << "Opt3: " << conf.GetDouble("option3") << endl;
cout << "Opt4: " << opt << endl;
return 0;
}
\endcode
Another possibility to access the result is the direct approach, for example:
\code
vector i = vm["option2"].as();
vector vec = vm["option6"].as>();
\endcode
Note that accessing an option which was not given will throw an exception.
Therefor its availability should first be checked in one of the following
ways:
\code
bool has_option1 = vm.count("option1");
bool has_option2 = conf.Has("option2");
\endcode
@section Examples
- An example can be found in \ref argv.cc
@todo Maybe we should remove the necessity to propagate argv[0] in the constructor?
*/
// **************************************************************************
#include "Configuration.h"
#include
#include
#include
#include
//#include
#include
#include
using namespace std;
namespace style = boost::program_options::command_line_style;
// --------------------------------------------------------------------------
//
//! The purpose of this function is basically to connect to the database,
//! and retrieve all the options entries from the 'Configuration' table.
//!
//! @param database
//! The URL of the database from which the configuration data is
//! retrieved. It should be given in the form
//! \li [user[:password]@]server.com[:port][/database]
//!
//! with
//! - user: user name (default is the current user)
//! - password: necessary if required by the database rights
//! - server: the URL of the server (can be 'localhost')
//! - port: the port to which to connect (usually obsolete)
//! - database: The name of the database containing the table
//!
//! @param desc
//! A reference to the object with the description of the options
//! which should be retrieved.
//!
//! @param allow_unregistered
//! If this is true also unregistered, i.e. options unknown to desc,
//! are returned. Otherwise an exception is thrown if such an option
//! was retrieved.
//!
//! @return
//! Return an object of type basic_parsed_options containing all
//! the entries retrieved from the database. Options not found in
//! desc are flagged as unregistered.
//!
//! @throws
//! Two types of exceptions are thrown
//! - It thows an unnamed exception if the options could not be
//! retrieved properly from the databse.
//! - If an option is not registered within the given descriptions
//! and \b allow_unregistered is \b false, an exception of type
//! \b po::unknown_option is thrown.
//!
//! @todo
//! - The exceptions handling should be improved.
//! - The final database layout is missing in the description
//! - Shell we allow options to be given more than once?
//
po::basic_parsed_options
Configuration::parse_database(const string &database, const po::options_description& desc, bool allow_unregistered)
{
//static const boost::regex expr("(([[:word:].-]+)(:(.+))?@)?([[:word:].-]+)(:([[:digit:]]+))?(/([[:word:].-]+))?");
static const boost::regex expr("(([[:word:].-]+)(:(.+))?@)?([[:word:].-]+)(:([[:digit:]]+))?(/([[:word:].-]+))");
// 2: user
// 4: pass
// 5: server
// 7: port
// 9: db
boost::smatch what;
if (!boost::regex_match(database, what, expr, boost::match_extra))
{
cout << "Couldn't parse '" << database << "'." << endl;
throw;
}
if (what.size()!=10)
{
cout << "Error parsing '" << database << "'." << endl;
throw;
}
const string user = what[2];
const string passwd = what[4];
const string server = what[5];
const string db = what[9];
const int port = atoi(string(what[7]).c_str());
cout << "Connecting to '";
if (!user.empty())
cout << user << "@";
cout << server;
if (port)
cout << ":" << port;
if (!db.empty())
cout << "/" << db;
cout << "'" << endl;
mysqlpp::Connection conn(db.c_str(), server.c_str(), user.c_str(), passwd.c_str(), port);
if (!conn.connected())
{
cout << "MySQL connection error: " << conn.error() << endl;
throw;
}
// Retrieve a subset of the sample stock table set up by resetdb
// and display it.
// FIXME: What about a prefix?
mysqlpp::Query query = conn.query("select `Key`, Value from Configuration");
mysqlpp::StoreQueryResult res = query.store();
if (!res)
{
cout << "MySQL query failed: " << query.error() << endl;
throw;
}
set allowed_options;
const vector> &options = desc.options();
for (unsigned i=0; i::iterator v=res.begin(); v Throw exception
continue;
// Check if we are allowed to accept unregistered options,
// i.e. options which are not in options_description &desc.
const bool unregistered = allowed_options.find(key)==allowed_options.end();
if (unregistered && allow_unregistered)
boost::throw_exception(po::unknown_option(key));
// Create a key/value-pair and store whether it is a
// registered option of not
po::option n;
n.string_key = key;
// This is now identical to file parsing. What if we want
// to concatenate options like on the command line?
n.value.clear(); // Fixme: composing?
n.value.push_back((*v)[1].c_str());
n.unregistered = unregistered;
// If any parsing will be done in the future...
//n.value().original_tokens.clear();
//n.value().original_tokens.push_back(name);
//n.value().original_tokens.push_back(value);
result.options.push_back(n);
}
cout << endl;
return result;
}
// --------------------------------------------------------------------------
//
//!
//
Configuration::Configuration(const string &prgname) : fName(prgname),
fNameMapper(bind1st(mem_fun(&Configuration::DefaultMapper), this))
{
po::options_description generic("Generic options");
generic.add_options()
("help", "Print available commandline options.")
("help-environment", "Print available environment variables.")
("help-database", "Print available options retreived from the database.")
("help-config", "Print available configuration file options.")
("version,V", "Print version information.")
("print-all", "Print all options as parsed from all the different sources.")
("print", "Print options as parsed from the commandline.")
("print-default", "Print options as parsed from default configuration file.")
("print-database", "Print options as retrieved from the database.")
("print-config", "Print options as parsed from the high priority configuration file.")
("print-environment", "Print options as parsed from the environment.")
("print-unknown", "Print unrecognized options.")
("print-options", "Print options as passed to program.")
("dont-check", "Do not check validity of options from files and database.")
("dont-check-files", "Do not check validity of options from files.")
("dont-check-database", "Do not check validity of options from database.")
;
po::options_description def_config;
def_config.add_options()
("default", var(prgname+string(".rc")), "Default configuration file.")
;
po::options_description config("Configuration options");
config.add_options()
("config,C", var(), "Configuration file overwriting options retrieved from the database.")
("database", var(), "Database link as in\n\t[user:[password]@][server][:port][/database]\nOverwrites options from the default configuration file.")
("no-database", "Suppress any access to the database even if a database URL was set.")
;
fOptionsCommandline[kVisible].add(generic);
fOptionsCommandline[kVisible].add(config);
fOptionsCommandline[kVisible].add(def_config);
fOptionsConfigfile[kVisible].add(config);
}
// --------------------------------------------------------------------------
//
//!
//
void Configuration::PrintParsed(const po::parsed_options &parsed) const
{
const vector< po::basic_option >& options = parsed.options;
// .description -> Pointer to opt_commandline
// const std::vector< shared_ptr >& options() const;
//const std::string& key(const std::string& option) const;
//const std::string& long_name() const;
//const std::string& description() const;
//shared_ptr semantic() const;
int maxlen = 0;
for (unsigned i=0; i &opt = options[i];
if (opt.value.size()>0 && opt.string_key[0]!='-')
Max(maxlen, opt.string_key.length());
}
cout.setf(ios_base::left);
// =============> Implement prining of parsed options
for(unsigned i=0; i &opt = options[i];
if (opt.value.size()==0 && !opt.string_key[0]=='-')
cout << "--";
cout << setw(maxlen) << opt.string_key;
if (opt.value.size()>0)
cout << " = " << opt.value[0];
//for (int j=0; j::iterator m=fVariables.begin();
m!=fVariables.end(); m++)
Max(maxlen, m->first.length());
cout.setf(ios_base::left);
// =============> Implement prining of options in use
for (map::iterator m=fVariables.begin();
m!=fVariables.end(); m++)
{
cout << setw(maxlen) << m->first << " = ";
const po::variable_value &v = m->second;
if (v.value().type()==typeid(bool))
cout << (v.as()?"true":"false") << " # bool";
if (v.value().type()==typeid(string))
cout << v.as() << " # string";
if (v.value().type()==typeid(int))
cout << v.as() << " # int";
if (v.value().type()==typeid(double))
cout << v.as() << " # double";
if (v.value().type()==typeid(float))
cout << v.as() << " # float";
if (v.value().type()==typeid(vector))
{
vector vec = v.as>();
for (vector::iterator s=vec.begin(); s))
{
vector vec = v.as>();
for (vector::iterator s=vec.begin(); s &vec, int steps)
{
for (vector::iterator v=vec.begin(); v &func)
{
fNameMapper = func;
}
void Configuration::SetNameMapper()
{
fNameMapper = bind1st(mem_fun(&Configuration::DefaultMapper), this);
}
void Configuration::SetPrintUsage(const boost::function &func)
{
fPrintUsage = func;
}
void Configuration::SetPrintUsage()
{
fPrintUsage = boost::bind(&Configuration::PrintUsage, this);
}
// --------------------------------------------------------------------------
//
//!
//! The idea of the Parse() memeber-function is to parse the command-line,
//! the configuration files, the databse and the environment and return
//! a proper combined result.
//!
//! In details the following actions are performed in the given order:
//!
//! - (0) Init local variables with the list of options described by the
//! data members.
//! - (1) Reset the data members fPriorityFile, fDefaultFile, fDatabase
//! - (2) Parse the command line
//! - (3) Check for \b --help* command-line options and performe
//! corresponding action
//! - (4) Check for \b --print and \b --print-all and perform corresponding
//! action
//! - (5) Read and parse the default configuration file, which is either
//! given by the default name or the \b --default command-line
//! option. The default name is compiled from the argument
//! given to the constructor and ".rc". If the file-name is
//! identical to the default (no command-line option given)
//! a missing configuration file is no error. Depending on
//! the \b --dont-check and \b --dont-check-files options,
//! unrecognized options in the file throw an exception or not.
//! - (6) Check for \b --print-default and \b --print-all and perform
//! corresponding action
//! - (7) Read and parse the priority configuration file, which must be given
//! by the \b --config or \b -C command-line option or a
//! corresponding entry in the default-configuration file.
//! If an option on the command-line and the in the configuration
//! file exists, the command-line option has priority.
//! If none is given, no priority file is read. Depending on
//! the \b --dont-check and \b --dont-check-files options,
//! unrecognized options in the file throw an exception or not.
//! - (8) Check for \b --print-config and \b --print-all and perform
//! corresponding action
//! - (9) Retrieve options from the database according to the
//! options \b --database and \b --no-database. Note that
//! options given on the command-line have highest priority.
//! The second priority is the priority-configuration file.
//! The options from the default configuration-file have
//! lowest priority.
//! - (10) Check for \b --print-database and \b --print-all and perform
//! corresponding action
//! - (11) Parse the environment options.
//! - (12) Check for \b --print-environment and \b --print-all and perform
//! corresponding action
//! - (13) Compile the final result. The priority of the options is (in
//! decreasing order): command-line options, options from the
//! priority configuration file, options from the database,
//! options from the default configuration-file and options
//! from the environment.
//! - (14) Finally all options which were found and flagged as unrecognized,
//! because they are not in the user-defined list of described
//! options, are collected and stored in the corresponding
//! data-members.
//! - (15) Before the function returns it check for \b --print-options
//! and \b --print-unknown and performs the corresponding actions.
//!
//!
//! @param argc,argv
//! arguments passed to main(int argc, char **argv)
//!
//! @returns
//! A reference to the list with the resulting options with their
//! values.
//!
//! @todo
//! - describe the exceptions
//! - describe what happens in a more general way
//! - print a waring when no default coonfig file is read
//! - proper handling and error messages if files not available
//
const po::variables_map &Configuration::Parse(int argc, char **argv)
{
const po::positional_options_description &opt_positional = fArgumentPositions;
// ------------------------ (0) --------------------------
po::options_description opt_commandline;
po::options_description opt_configfile;
po::options_description opt_environment;
po::options_description opt_database;
for (int i=0; i<2; i++)
{
opt_commandline.add(fOptionsCommandline[i]);
opt_configfile.add(fOptionsConfigfile[i]);
opt_environment.add(fOptionsEnvironment[i]);
opt_database.add(fOptionsDatabase[i]);
}
// ------------------------ (1) --------------------------
fPriorityFile = "";
fDefaultFile = "";
fDatabase = "";
// ------------------------ (2) --------------------------
po::command_line_parser parser(argc, argv);
parser.options(opt_commandline);
parser.positional(opt_positional);
parser.style(style::unix_style&~style::allow_guessing);
//parser.allow_unregistered();
const po::parsed_options parsed_commandline = parser.run();
// ------------------------ (3) --------------------------
po::variables_map getfiles;
po::store(parsed_commandline, getfiles);
if (getfiles.count("help"))
{
fPrintUsage();
cout << fOptionsCommandline[kVisible] << endl;
}
if (getfiles.count("help-config"))
cout << fOptionsConfigfile[kVisible] << endl;
if (getfiles.count("help-env"))
cout << fOptionsEnvironment[kVisible] << endl;
if (getfiles.count("help-database"))
cout << fOptionsDatabase[kVisible] << endl;
// ------------------------ (4) --------------------------
if (getfiles.count("print") || getfiles.count("print-all"))
{
cout << endl << "Parsed commandline options:" << endl;
PrintParsed(parsed_commandline);
cout << endl;
}
// ------------------------ (5) --------------------------
// Get default file from command line
if (getfiles.count("default"))
{
fDefaultFile = getfiles["default"].as();
cerr << "Reading configuration from '" << fDefaultFile << "'." << endl;
}
const bool checkf = !getfiles.count("dont-check-files") && !getfiles.count("dont-check");
const bool defaulted = getfiles.count("default") && getfiles["default"].defaulted();
//const bool exists = boost::filesystem::exists(fDefaultFile);
ifstream indef(fDefaultFile.c_str());
// ===> FIXME: Proper handling of missing file or wrong file name
const po::parsed_options parsed_defaultfile =
!indef && defaulted ?
po::parsed_options(&opt_configfile) :
po::parse_config_file(indef, opt_configfile, !checkf);
// ------------------------ (6) --------------------------
if (getfiles.count("print-default") || getfiles.count("print-all"))
{
if (!indef && defaulted)
cout << "No configuration file by --default option specified." << endl;
else
{
cout << endl << "Parsed options from '" << fDefaultFile << "':" << endl;
PrintParsed(parsed_defaultfile);
cout << endl;
}
}
po::store(parsed_defaultfile, getfiles);
// ------------------------ (7) --------------------------
// Get priority from commandline(1), defaultfile(2)
if (getfiles.count("config"))
{
fPriorityFile = getfiles["config"].as();
cerr << "Retrieved option from '" << fPriorityFile << "'." << endl;
}
ifstream inpri(fPriorityFile.c_str());
// ===> FIXME: Proper handling of missing file or wrong file name
const po::parsed_options parsed_priorityfile =
fPriorityFile.empty() ? po::parsed_options(&opt_configfile) :
po::parse_config_file(inpri, opt_configfile, !checkf);
// ------------------------ (8) --------------------------
if (getfiles.count("print-config") || getfiles.count("print-all"))
{
if (fPriorityFile.empty())
cout << "No configuration file by --config option specified." << endl;
else
{
cout << endl << "Parsed options from '" << fPriorityFile << "':" << endl;
PrintParsed(parsed_priorityfile);
cout << endl;
}
}
// ------------------------ (9) --------------------------
po::variables_map getdatabase;
po::store(parsed_commandline, getdatabase);
po::store(parsed_priorityfile, getdatabase);
po::store(parsed_defaultfile, getdatabase);
if (getdatabase.count("database") && !getdatabase.count("no-database"))
{
fDatabase = getdatabase["database"].as();
cerr << "Retrieving configuration from '" << fDatabase << "'." << endl;
}
const bool checkdb = !getdatabase.count("dont-check-database") && !getdatabase.count("dont-check");
const po::parsed_options parsed_database =
fDatabase.empty() ? po::parsed_options(&opt_database) :
parse_database(fDatabase, opt_database, !checkdb);
// ------------------------ (10) -------------------------
if (getfiles.count("print-database") || getfiles.count("print-all"))
{
if (fDatabase.empty())
cout << "No database access requested." << endl;
else
{
cout << endl << "Options retrieved from '" << fDatabase << "':" << endl;
PrintParsed(parsed_database);
cout << endl;
}
}
// ------------------------ (11) -------------------------
const po::parsed_options parsed_environment = po::parse_environment(opt_environment, fNameMapper);
// ------------------------ (12) -------------------------
if (getfiles.count("print-environment"))
{
cout << "Parsed options from environment:" << endl;
PrintParsed(parsed_environment);
cout << endl;
}
// ------------------------ (13) -------------------------
po::variables_map result;
po::store(parsed_commandline, result);
po::store(parsed_priorityfile, result);
po::store(parsed_database, result);
po::store(parsed_defaultfile, result);
po::store(parsed_environment, result);
po::notify(result);
fVariables = result;
// ------------------------ (14) -------------------------
const vector unknown1 = collect_unrecognized(parsed_defaultfile.options, po::exclude_positional);
const vector unknown2 = collect_unrecognized(parsed_priorityfile.options, po::exclude_positional);
fUnknownConfigfile.clear();
fUnknownConfigfile.insert(fUnknownConfigfile.end(), unknown1.begin(), unknown1.end());
fUnknownConfigfile.insert(fUnknownConfigfile.end(), unknown2.begin(), unknown2.end());
fUnknownCommandline = collect_unrecognized(parsed_commandline.options, po::exclude_positional);
fUnknownEnvironment = collect_unrecognized(parsed_environment.options, po::exclude_positional);
fUnknownDatabase = collect_unrecognized(parsed_database.options, po::exclude_positional);
// ------------------------ (15) -------------------------
if (result.count("print-options"))
PrintOptions();
if (result.count("print-unknown"))
PrintUnknown();
return fVariables;
}