boost :: program_options и несколько разделов в ini-файле

Я пытаюсь получить boost :: program_options для чтения INI-файла с несколькими разделами:

[slave]
address=localhost
port=1111

[slave]
address=192.168.0.1
port=2222

Есть ли решение?

Заранее спасибо!

4

Решение

Есть несколько решений этой проблемы. Хотя на первый взгляд может показаться, что это должно быть легкой задачей, это часто довольно сложно. Это потому, что разделы примерно эквивалентны пространствам имен; разделы не эквивалентны объектам.

[Ведомый]
Адрес = локальный
Порт = 1111

[Ведомый]
адрес = 192.168.0.1
Порт = 2222

Вышеуказанная конфигурация имеет один slave пространство имен, которое содержит два address значения и два port ценности. Нет двух slave объекты, каждый из которых имеет address а также port, Из-за этого различия в коде приложения должны выполняться ассоциированные значения или спаривание. Это представляет следующие варианты:

  • Используйте макет файла конфигурации, чтобы подразумевать сопряжение.
  • Выполните явное сопряжение, объединив несколько значений в одно значение поля.

Подразумеваемое сопряжение

При таком подходе файл конфигурации может оставаться как есть. Простота этого подхода зависит от:

  • Поведение нескольких компонентов Boost.ProgramOption.
  • Каждый объект представлен в виде раздела, не имеющего необязательных полей и небольшого количества полей.
[Ведомый]
address = localhost # slave.address [0]
port = 1111 # slave.port [0]

[Ведомый]
address = 192.168.0.1 # slave.address [1]
port = 2222 # slave.port [1]

Без изменения конфигурации, следующий код:

#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" ) );
}

Производит этот вывод:

Адрес ведомого: localhost, порт: 1111
Адрес ведомого: 192.168.0.1, порт: 2222

Основное явное сопряжение

Несколько значений могут быть иногда представлены в одном поле значимым образом. Одно общее представление обоих address а также port является address:port, При таком сопряжении результирующий файл конфигурации будет выглядеть так:

[рабы]
раб = локальный: 1111
раб = 192.168.0.1: 2222

Эта простота этого подхода зависит от:

  • Возможность представлять несколько значений как одно значимое значение без ключевых спецификаторов.
  • Каждый объект не имеет дополнительных значений.

Обновленный код:

#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" ) );
}

Выдает тот же результат:

Адрес ведомого: localhost, порт: 1111
Адрес ведомого: 192.168.0.1, порт: 2222

И заметные модификации кода следующие:

  • options_description«s options нужно читать slaves.slave как std::vector< std::string >,
  • make_slave займет один std::string аргумент, из которого он будет извлекать address а также port,
  • Обновите std::transform вызовите только итерации по одному диапазону.

Расширенное явное сопряжение

Часто несколько полей не могут быть осмысленно представлены как одно значение без ключа, или объект имеет необязательные поля. В этих случаях необходим дополнительный уровень синтаксиса и синтаксического анализа. Хотя приложения могут вводить свой собственный синтаксис и синтаксические анализаторы, я предлагаю использовать синтаксис командной строки Boost.ProgramOption (--key value а также --key=value) и парсеры. Полученный файл конфигурации может выглядеть так:

[рабы]
slave = --адрес localhost --port 1111
slave = --адрес = 192.168.0.1 --port = 2222

Обновленный код:

#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" ) );
}

Создает тот же результат, что и предыдущие подходы:

Адрес ведомого: localhost, порт: 1111
Адрес ведомого: 192.168.0.1, порт: 2222

И заметные модификации кода следующие:

  • созданный copy_if так как это был забытый алгоритм в C ++ 03.
  • Использование Boost.Tokenizer вместо Boost.StringAlgo, поскольку Boost.Tokenizer легче обрабатывает экранирование в кавычках.
  • Создано option_builder унарный функтор, чтобы помочь обеспечить идиоматическое повторное использование для применения преобразований.
  • make_slave сейчас занимает boost::program_options::variables_map из которого он будет строить slave объект.

Этот подход также может быть легко расширен для поддержки следующих вариантов:

  • Поддержка нескольких командных строк для одного значения. Например, конфигурация может поддерживать двух подчиненных, причем один из подчиненных имеет вторичную конфигурацию в случае сбоя первого. Это требует выполнения начальной токенизации на , разделитель.

    [рабы]
    slave = --адрес localhost --port 1111, --адрес 127.0.0.1 --port 1112
    slave = - адрес 192.168.0.1 - порт 2222
  • Объявление вариантов для slave_desc как typed_value с переменными, предоставленными store_to аргумент. Эти же переменные могут быть связаны с boost::ref с помощью boost::bind к make_slave заводская функция. В то время как это разъединяет make_slave из типов Boost.ProgramOptions может быть сложно поддерживать типы с множеством полей.


Альтернативные подходы

Альтернативные подходы все еще нуждаются в явном сопряжении путем помещения нескольких значений в одно значение. Однако преобразования могут происходить на этапе синтаксического анализа, наследуя от boost::program_options::typed_value или же boost::program_options::untyped_value,

  • При наследовании от typed_valueпереопределить parse функция. Одним из последствий использования typed_value является то, что параметр шаблона должен соответствовать всем требованиям для typed_value, Например, если typed_value< slave > был использован, то это потребовало бы slave конструируемый по умолчанию и определяющий оба istream извлечение (>>) а также ostream вставка (<<) операторы для slave,
  • При наследовании от untyped_valueОтменить оба parse а также notify функции. Этот подход не навязывает требования типа, такие как typed_value, но он требует, чтобы производный класс сохранял свой собственный store_to переменная.

Предложения

  • Если абсолютная уверенность в том, что никогда не будет необязательного поля, а количество полей будет минимальным (2 ~), используйте подход подразумеваемого спаривания.
  • Если будет минимальное количество полей (2 ~) и значения могут быть представлены значимым образом без идентификаторов имен полей, используйте базовое явное сопряжение. Дополнительные поля могут поддерживаться, но это увеличивает сложность как синтаксиса, так и синтаксического анализатора.
  • Для всех других случаев, или когда есть какая-либо неопределенность, используйте расширенное явное сопряжение. Хотя это может занять немного больше работы, это обеспечивает большую возможность повторного использования. Например, если конфигурации ведомого устройства становятся настолько сложными, что у каждого ведомого устройства есть свой собственный файл конфигурации, то изменения кода минимальны, так как необходимо изменить только тип анализатора и вызов.
14

Другие решения

Других решений пока нет …