I'm trying to get boost::program_options to read a ini file with multiple sections:
[slave]
address=localhost
port=1111
[slave]
address=192.168.0.1
port=2222
Is there any solution?
Thanks in advance!
There are a few solutions to this problem. While it may initially appear that this should be an easy task, it is often fairly involved. This is because sections are roughly equivalent to namespaces; sections are not equivalent to objects.
[slave] address=localhost port=1111 [slave] address=192.168.0.1 port=2222
The above configuration has a single slave namespace, that contains two address values and two port values.  There are not two slave objects that each have an address and port.  Due to this distinction, associating values, or pairing, must be done in the application code.  This presenting the following options:
With this approach, the configuration file can remain as-is. The simplicity of this approach depends on:
[slave] address=localhost # slave.address[0] port=1111 # slave.port[0] [slave] address=192.168.0.1 # slave.address[1] port=2222 # slave.port[1]
Without modifying the configuration, the following code:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/program_options.hpp>
/// @brief Convenience function for when a 'store_to' value is being provided
///        to typed_value.
///
/// @param store_to The variable that will hold the parsed value upon notify.
///
/// @return Pointer to a type_value.
template < typename T >
boost::program_options::typed_value< T >* make_value( T* store_to )
{
  return boost::program_options::value< T >( store_to );
}
/// @brief Slave type that contains an address and port.
struct slave
{
  std::string    address;
  unsigned short port;
  /// @brief Constructor.
  slave( std::string address, 
         unsigned short port )
    : address( address ),
      port( port )
  {}
};
/// @brief Stream insertion operator for slave.
///
/// @param stream The stream into which slave is being inserted.
/// @param s The slave object.
///
/// @return Reference to the ostream.
std::ostream& operator<<( std::ostream& stream, 
                          const slave& slave )
{
  return stream << "Slave address: " << slave.address 
                << ", port: "        << slave.port;
}
/// @brief Makes a slave given an address and port.
slave make_slave( const std::string& address,
                  unsigned short port )
{
  return slave( address, port );
}
int main()
{
  // Variables that will store parsed values.
  std::vector< std::string >    addresses;
  std::vector< unsigned short > ports;
  // Setup options.
  namespace po = boost::program_options;
  po::options_description desc( "Options" );
  desc.add_options()
    ( "slave.address", make_value( &addresses ),
                       "slave's hostname or ip address" )
    ( "slave.port"   , make_value( &ports ),
                       "plugin id" );
  // Load setting file.
  po::variables_map vm;
  std::ifstream settings_file( "config.ini", std::ifstream::in );
  po::store( po::parse_config_file( settings_file , desc ), vm );
  settings_file.close();
  po::notify( vm );
  // Transform each address and port pair into a slave via make_slave,
  // inserting each object into the slaves vector.
  std::vector< slave > slaves;
  std::transform( addresses.begin(), addresses.end(),
                  ports.begin(),
                  std::back_inserter( slaves ),
                  make_slave );
  // Print slaves.
  std::copy( slaves.begin(), slaves.end(), 
             std::ostream_iterator< slave >( std::cout, "\n" ) );
}
Produces this output:
Slave address: localhost, port: 1111 Slave address: 192.168.0.1, port: 2222
Multiple values can be occasionally be represented within a single field in a meaningful way.   One common representation of both address and port is address:port.  With this pairing, the resulting configuration file would like:
[slaves] slave=localhost:1111 slave=192.168.0.1:2222
This simplicity of this approach depends upon:
The updated code:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/program_options.hpp>
/// @brief Convenience function for when a 'store_to' value is being provided
///        to typed_value.
///
/// @param store_to The variable that will hold the parsed value upon notify.
///
/// @return Pointer to a type_value.
template < typename T >
boost::program_options::typed_value< T >* make_value( T* store_to )
{
  return boost::program_options::value< T >( store_to );
}
/// @brief Slave type that contains an address and port.
struct slave
{
  std::string    address;
  unsigned short port;
  /// @brief Constructor.
  slave( std::string address, 
         unsigned short port )
    : address( address ),
      port( port )
  {}
};
/// @brief Stream insertion operator for slave.
///
/// @param stream The stream into which slave is being inserted.
/// @param s The slave object.
///
/// @return Reference to the ostream.
std::ostream& operator<<( std::ostream& stream, 
                          const slave& slave )
{
  return stream << "Slave address: " << slave.address 
                << ", port: "        << slave.port;
}
/// @brief Makes a slave given an address and port.
slave make_slave( const std::string& address_and_port )
{
  // Tokenize the string on the ":" delimiter. 
  std::vector< std::string > tokens;
  boost::split( tokens, address_and_port, boost::is_any_of( ":" ) );
  // If the split did not result in exactly 2 tokens, then the value
  // is formatted wrong.
  if ( 2 != tokens.size() )
  {
     using boost::program_options::validation_error;
     throw validation_error( validation_error::invalid_option_value,
                             "slaves.slave",
                             address_and_port );
  }
  // Create a slave from the token values.
  return slave( tokens[0],
                boost::lexical_cast< unsigned short >( tokens[1] ) );
}
int main()
{
  // Variables that will store parsed values.
  std::vector< std::string > slave_configs;
  // Setup options.
  namespace po = boost::program_options;
  po::options_description desc( "Options" );
  desc.add_options()
    ( "slaves.slave", make_value( &slave_configs ),
                      "slave's address@port" );
  // Load setting file.
  po::variables_map vm;
  std::ifstream settings_file( "config.ini", std::ifstream::in );
  po::store( po::parse_config_file( settings_file , desc ), vm );
  settings_file.close();
  po::notify( vm );
  // Transform each config into a slave via make_slave, inserting each 
  // object into the slaves vector.
  std::vector< slave > slaves;
  std::transform( slave_configs.begin(), slave_configs.end(),
                  std::back_inserter( slaves ),
                  make_slave );
  // Print slaves.
  std::copy( slaves.begin(), slaves.end(), 
             std::ostream_iterator< slave >( std::cout, "\n" ) );
}
Produces the same output:
Slave address: localhost, port: 1111 Slave address: 192.168.0.1, port: 2222
And the notable code modifications are as follows:
options_description's options need to be reading slaves.slave as a std::vector< std::string >.make_slave will take a single std::string argument, from which it will extract address and port.std::transform call to only iterate over one range.Often, multiple fields cannot be represented meaningfully as a single key-less value, or an object has optional fields.  For these cases, an additional level of syntax and parsing needs to occur.  While applications can introduce their own syntax and parsers, I suggest leveraging Boost.ProgramOption's command line syntax (--key value and --key=value) and parsers.  The resulting configuration file could look like:
[slaves] slave= --address localhost --port 1111 slave= --address = 192.168.0.1 --port=2222
The updated code:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/bind.hpp>
#include <boost/program_options.hpp>
#include <boost/tokenizer.hpp>
// copy_if was accidently left out of the C++03 standard, so mimic the
// C++11 behavior to support all predicate types.  The alternative is to
// use remove_copy_if, but it only works for adaptable functors.
template < typename InputIterator,
           typename OutputIterator, 
           typename Predicate >
OutputIterator 
copy_if( InputIterator first,
         InputIterator last,
         OutputIterator result,
         Predicate pred )
{
  while( first != last )
  {
    if( pred( *first ) )
      *result++ = *first;
    ++first;
  }
  return result;
}
/// @brief Tokenize a string.  The tokens will be separated by each non-quoted
///        character in @c separator_characters.  Empty tokens are removed.
///
/// @param input The string to tokenize.
/// @param separator_characters The characters on which to delimit.
///
/// @return Vector of tokens.
std::vector< std::string > tokenize( const std::string& input,
                                     const std::string& separator_characters )
{
   typedef boost::escaped_list_separator< char > separator_type;
   separator_type separator( "\\", // The escape characters.
                             separator_characters,
                             "\"\'" ); // The quote characters.
   // Tokenize the intput.
   boost::tokenizer< separator_type > tokens( input, separator );
   // Copy non-empty tokens from the tokenizer into the result.
   std::vector< std::string > result;
   copy_if( tokens.begin(), tokens.end(), std::back_inserter( result ), 
            !boost::bind( &std::string::empty, _1 ) );
   return result;
}
/// @brief option_builder provides a unary operator that can be used within
///        stl::algorithms.
template < typename ResultType,
           typename Builder >
class option_builder
{
public:
  typedef ResultType result_type;
public:
  /// @brief Constructor
  option_builder( const boost::program_options::options_description& options,
                  Builder builder )
    : options_( options ),
      builder_( builder )
  {}
  /// @brief Unary operator that will parse @c value, then delegate the
  ///        construction of @c result_type to the builder.
  template < typename T >
  result_type operator()( const T& value )
  {
    // Tokenize the value so that the command line parser can be used.
    std::vector< std::string > tokens = tokenize( value, "= " );
    // Parse the tokens.
    namespace po = boost::program_options;
    po::variables_map vm;
    po::store( po::command_line_parser( tokens ).options( options_ ).run(),
               vm );
    po::notify( vm );
    // Delegate object construction to the builder.
    return builder_( vm );
  }
private:
  const boost::program_options::options_description& options_;
  Builder builder_;
};
/// @brief  Convenience function used to create option_builder types.
template < typename T,
           typename Builder >
option_builder< T, Builder > make_option_builder(
  const boost::program_options::options_description& options,
  Builder builder )
{
  return option_builder< T, Builder >( options, builder );
}
/// @brief Convenience function for when a 'store_to' value is being provided
///        to typed_value.
///
/// @param store_to The variable that will hold the parsed value upon notify.
///
/// @return Pointer to a type_value.
template < typename T >
boost::program_options::typed_value< T >* make_value( T* store_to )
{
  return boost::program_options::value< T >( store_to );
}
/// @brief Slave type that contains an address and port.
struct slave
{
  std::string    address;
  unsigned short port;
  /// @brief Constructor.
  slave( std::string address, 
         unsigned short port )
    : address( address ),
      port( port )
  {}
};
/// @brief Stream insertion operator for slave.
///
/// @param stream The stream into which slave is being inserted.
/// @param s The slave object.
///
/// @return Reference to the ostream.
std::ostream& operator<<( std::ostream& stream, 
                          const slave& slave )
{
  return stream << "Slave address: " << slave.address 
                << ", port: "        << slave.port;
}
/// @brief Makes a slave given an address and port.
slave make_slave( const boost::program_options::variables_map& vm )
{
  // Create a slave from the variable map.
  return slave( vm["address"].as< std::string >(),
                vm["port"].as< unsigned short >() );
}
int main()
{
  // Variables that will store parsed values.
  std::vector< std::string > slave_configs;
  // Setup options.
  namespace po = boost::program_options;
  po::options_description desc( "Options" );
  desc.add_options()
    ( "slaves.slave", make_value( &slave_configs ),
                      "slave's --address ip/hostname --port num" );
  // Load setting file.
  po::variables_map vm;
  std::ifstream settings_file( "config.ini", std::ifstream::in );
  po::store( po::parse_config_file( settings_file , desc ), vm );
  settings_file.close();
  po::notify( vm );
  // Create options for slaves.slave.
  po::options_description slave_desc( "Slave Options" );
  slave_desc.add_options()
    ( "address", po::value< std::string >(),
                 "slave's hostname or ip address" )
    ( "port"   , po::value< unsigned short >(),
                 "slave's port" );
  // Transform each config into a slave via creating an option_builder that
  // will use the slave_desc and make_slave to create slave objects.  These
  // objects will be inserted into the slaves vector.
  std::vector< slave > slaves;
  std::transform( slave_configs.begin(), slave_configs.end(),
                  std::back_inserter( slaves ),
                  make_option_builder< slave >( slave_desc, make_slave ) );
  // Print slaves.
  std::copy( slaves.begin(), slaves.end(), 
             std::ostream_iterator< slave >( std::cout, "\n" ) ); 
}
Produces the same output as the previous approaches:
Slave address: localhost, port: 1111 Slave address: 192.168.0.1, port: 2222
And the notable code modifications are as follows:
copy_if since it was an overlooked algorithm in C++03.option_builder unary functor to help provide idiomatic reuse for applying transformations.make_slave now takes a boost::program_options::variables_map from which it will construct a slave object.This approach can also easily be extended to support the following variations:
Supporting multiple command-lines for a single value.  For example, a configuration could support two slaves, with one of the slaves having a secondary configuration in case the first fails.  This requires performing an initial tokenization on the , delimiter.
[slaves] slave = --address localhost --port 1111, --address 127.0.0.1 --port 1112 slave = --address 192.168.0.1 --port 2222
Declaring the options for slave_desc as typed_value with variables provided to the store_to argument.  These same variables can then be bound with boost::ref via boost::bind to the make_slave factory function.  While this decouples make_slave from Boost.ProgramOptions types, it may become difficult to maintain for types with many fields.
Alternative approaches still need explicit pairing to be done via placing multiple values into a single value.  However, transformations can occur during the parsing phase by inheriting from either boost::program_options::typed_value or boost::program_options::untyped_value.
typed_value, override the parse function.  One consequence of using typed_value is that the template parameter must meet all the requirements for typed_value.  For example, if typed_value< slave > was used, then it would require making slave default constructable, and defining both istream extraction (>>) and ostream insertion (<<) operators for slave.untyped_value, override both the parse and notify functions.  This approach does not impose type requirements like typed_value, but it does require that the derived class maintain its own store_to variable.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With