How to add a description to boost::program_options' positional options? - c++

I would like to make a positional, list program option with boost_program_options that do not allow named program options (like --files).
I have the following snippet of code:
#include <boost/program_options.hpp>
#include <iostream>
#include <string>
#include <vector>
namespace po = boost::program_options;
int main(int argc, const char* argv[]) {
po::options_description desc("Allowed options");
desc.add_options()("help", "produce help message")
( "files", po::value<std::vector<std::string>>()->required(), "list of files");
po::positional_options_description pos;
pos.add("files", -1);
po::variables_map vm;
try {
po::store(po::command_line_parser(argc, argv).options(desc).positional(pos).run(), vm);
po::notify(vm);
} catch(const po::error& e) {
std::cerr << "Couldn't parse command line arguments properly:\n";
std::cerr << e.what() << '\n' << '\n';
std::cerr << desc << '\n';
return 1;
}
if(vm.count("help") || !vm.count("files")) {
std::cout << desc << "\n";
return 1;
}
}
The problem is that I can read files list as positional arguments lists as follows:
./a.out file1 file2 file3
but unfortunately like this as well ( which I would like to disable )
./a.out --files file1 file2 file3
The problem is also with the help which yields:
./a.out
Couldn't parse command line arguments properly:
the option '--files' is required but missing
Allowed options:
--help produce help message
--files arg list of files
So my desired scenario would be more like (os similar):
./a.out
Couldn't parse command line arguments properly:
[FILES ...] is required but missing
Allowed options:
--help produce help message
--optionx some random option used in future
[FILE ...] list of files
After I remove files options from desc.add_option()(...) it stop working so I believe I need it there.

As to the question posed in the title, "How to add a description to boost::program_options' positional options?", there's no functionality provided for this in the library. You need to handle that part yourself.
As for the body of the question... it's possible, but in a slightly round-about way.
The positional options map each position to a name, and the names need to exist. From what I can tell in the code (cmdline.cpp), the unregistered flag won't be set for arguments that are positional. [1], [2]
So, to do what you want, we can do the following:
Hide the --files option from showing up in the help. You will need to display appropriate help for the positional options yourself, but this is no different than before.
Add our own validation between parsing and storing of the parsed options to the variables_map.
Hiding --files from help
Here we take advantage of the the fact that we can create composite options_description using the add(...) member function:
po::options_description desc_1;
// ...
po::options_description desc_2;
// ...
po::options_description desc_composite;
desc_composite.add(desc_1).add(desc_2);
We can therefore place our files option into a hidden options_description, and create a composite that we will use only for the parsing stage. (see code below)
Preventing explicit --files
We need to intercept the list of options between parsing and storing them into the variables_map.
The run() method of command_line_parser returns an instance of basic_parsed_options, whose member options holds a vector of basic_options. There is an element for each parsed argument, and any positional options are enumerated starting from 0, any non-positional options have position -1. We can use this to perform our own validation and raise an error when we see --files as an explicit (non-positional) argument.
Example Source Code
See on Coliru
#include <boost/program_options.hpp>
#include <iostream>
#include <string>
#include <vector>
namespace po = boost::program_options;
int main(int argc, const char* argv[])
{
std::vector<std::string> file_names;
po::options_description desc("Allowed options");
desc.add_options()
("help", "produce help message")
("test", "test option");
std::string const FILES_KEY("files");
// Hide the `files` options in a separate description
po::options_description desc_hidden("Hidden options");
desc_hidden.add_options()
(FILES_KEY.c_str(), po::value(&file_names)->required(), "list of files");
// This description is used for parsing and validation
po::options_description cmdline_options;
cmdline_options.add(desc).add(desc_hidden);
// And this one to display help
po::options_description visible_options;
visible_options.add(desc);
po::positional_options_description pos;
pos.add(FILES_KEY.c_str(), -1);
po::variables_map vm;
try {
// Only parse the options, so we can catch the explicit `--files`
auto parsed = po::command_line_parser(argc, argv)
.options(cmdline_options)
.positional(pos)
.run();
// Make sure there were no non-positional `files` options
for (auto const& opt : parsed.options) {
if ((opt.position_key == -1) && (opt.string_key == FILES_KEY)) {
throw po::unknown_option(FILES_KEY);
}
}
po::store(parsed, vm);
po::notify(vm);
} catch(const po::error& e) {
std::cerr << "Couldn't parse command line arguments properly:\n";
std::cerr << e.what() << '\n' << '\n';
std::cerr << visible_options << '\n';
return 1;
}
if (vm.count("help") || !vm.count("files")) {
std::cout << desc << "\n";
return 1;
}
if (!file_names.empty()) {
std::cout << "Files: \n";
for (auto const& file_name : file_names) {
std::cout << " * " << file_name << "\n";
}
}
}
Test Output
Valid options:
>example a b c --test d e
Files:
* a
* b
* c
* d
* e
Invalid options:
>example a b c --files d e
Couldn't parse command line arguments properly:
unrecognised option 'files'
Allowed options:
--help produce help message
--test test option

Related

boost program_options: Read required parameter from config file

I want to use boost_program_options as follows:
get name of an optional config file as a program option
read mandatory options either from command line or the config file
The problem is: The variable containing the config file name is not populated until po::notify() is called, and that function also throws exceptions for any unfulfilled mandatory options. So if the mandatory options are not specified on the command line (rendering the config file moot), the config file is not read.
The inelegant solution is to not mark the options as mandatory in add_options(), and enforce them 'by hand' afterwards. Is there a solution to this within the boost_program_options library?
MWE
bpo-mwe.conf:
db-hostname = foo
db-username = arthurdent
db-password = forty-two
Code:
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <boost/program_options.hpp>
// enable/disable required() below
#ifndef WITH_REQUIRED
#define WITH_REQUIRED
#endif
namespace po = boost::program_options;
namespace fs = std::filesystem;
int main(int argc, char *argv[])
{
std::string config_file;
po::options_description generic("Generic options");
generic.add_options()
("config,c", po::value<std::string>(&config_file)->default_value("bpo-mwe.conf"), "configuration file")
;
// Declare a group of options that will be
// allowed both on command line and in
// config file
po::options_description main_options("Main options");
main_options.add_options()
#ifdef WITH_REQUIRED
("db-hostname", po::value<std::string>()->required(), "database service name")
("db-username", po::value<std::string>()->required(), "database user name")
("db-password", po::value<std::string>()->required(), "database user password")
#else
("db-hostname", po::value<std::string>(), "database service name")
("db-username", po::value<std::string>(), "database user name")
("db-password", po::value<std::string>(), "database user password")
#endif
;
// set options allowed on command line
po::options_description cmdline_options;
cmdline_options.add(generic).add(main_options);
// set options allowed in config file
po::options_description config_file_options;
config_file_options.add(main_options);
// set options shown by --help
po::options_description visible("Allowed options");
visible.add(generic).add(main_options);
po::variables_map variable_map;
// store command line options
// Why not po::store?
//po::store(po::parse_command_line(argc, argv, desc), vm);
store(po::command_line_parser(argc, argv).options(cmdline_options).run(), variable_map);
notify(variable_map); // <- here is the problem point
// Problem: config_file is not set until notify() is called, and notify() throws exception for unfulfilled required variables
std::ifstream ifs(config_file.c_str());
if (!ifs)
{
std::cout << "can not open configuration file: " << config_file << "\n";
}
else
{
store(parse_config_file(ifs, config_file_options), variable_map);
notify(variable_map);
}
std::cout << config_file << " was the config file\n";
return 0;
}
I'd simply not use the notifying value-semantic to put the value in config_file. Instead, use it directly from the map:
auto config_file = variable_map.at("config").as<std::string>();
Now you can do the notify at the end, as intended:
Live On Coliru
#include <boost/program_options.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>
namespace po = boost::program_options;
int main(int argc, char *argv[])
{
po::options_description generic("Generic options");
generic.add_options()
("config,c", po::value<std::string>()->default_value("bpo-mwe.conf"), "configuration file")
;
// Declare a group of options that will be allowed both on command line and
// in config file
struct {
std::string host, user, pass;
} dbconf;
po::options_description main_options("Main options");
main_options.add_options()
("db-hostname", po::value<std::string>(&dbconf.host)->required(), "database service name")
("db-username", po::value<std::string>(&dbconf.user)->required(), "database user name")
("db-password", po::value<std::string>(&dbconf.pass)->required(), "database user password")
;
// set options allowed on command line
po::options_description cmdline_options;
cmdline_options.add(generic).add(main_options);
// set options allowed in config file
po::options_description config_file_options;
config_file_options.add(main_options);
// set options shown by --help
po::options_description visible("Allowed options");
visible.add(generic).add(main_options);
po::variables_map variable_map;
//po::store(po::parse_command_line(argc, argv, desc), vm);
store(po::command_line_parser(argc, argv).options(cmdline_options).run(),
variable_map);
auto config_file = variable_map.at("config").as<std::string>();
std::ifstream ifs(config_file.c_str());
if (!ifs) {
std::cout << "can not open configuration file: " << config_file << "\n";
} else {
store(parse_config_file(ifs, config_file_options), variable_map);
notify(variable_map);
}
notify(variable_map);
std::cout << config_file << " was the config file\n";
std::cout << "dbconf: " << std::quoted(dbconf.host) << ", "
<< std::quoted(dbconf.user) << ", "
<< std::quoted(dbconf.pass) << "\n"; // TODO REMOVE FOR PRODUCTION :)
}
Prints eg.
$ ./sotest
bpo-mwe.conf was the config file
dbconf: "foo", "arthurdent", "forty-two"
$ ./sotest -c other.conf
other.conf was the config file
dbconf: "sbb", "neguheqrag", "sbegl-gjb"
$ ./sotest -c other.conf --db-user PICKME
other.conf was the config file
dbconf: "sbb", "PICKME", "sbegl-gjb"
Where as you might have guessed other.conf is derived from bpo-mwe.conf by ROT13.
Differentiate between configuration file and command-line arguments, don't parse both into the same map.
Instead first parse the command-line arguments separately, get the configuration file name (if there is any) and then load the file and parse it into a second map.
If some configuration-file values can be provided on the command line as well, then I personally do two passes over the command-line arguments, making it a three-step process:
Parse command-line arguments, ignore all but the config option
Read and parse the configuration file
And do a second pass over the command-line arguments, ignoring the config option

Sane way to use boost::program_options with INI and command line?

I want to write a program that parses a config file, and allows the command line to override what's written there. So I can have a config file that says:
[section1]
opt1=42
[section2]
opt2=17
And then, I can run the command with:
./so --opt2=3
And the program will get opt1 as 42 and opt2 and 3. I use the following program to try and do it:
#include <fstream>
#include <boost/program_options.hpp>
namespace po = boost::program_options;
int main(int argc, char *argv[]) {
po::options_description options1("section1");
options1.add_options()
("opt1", po::value<int>(), "Option 1");
po::options_description options2("section2");
options2.add_options()
("opt2", po::value<int>(), "Option 2");
po::options_description options;
options.add(options1);
options.add(options2);
po::variables_map values;
po::store( po::command_line_parser( argc, argv ).options(options).run(), values );
std::ifstream iniFile( "options.ini" );
po::store(
parse_config_file( iniFile, options ),
values );
}
This, of course, doesn't work. Boost::program_options wants opt1 under section1 to be called section1.opt1. If I do that, however, my program becomes harder to maintain on two fronts:
I need to define two options_descriptions, one for the INI and one for the command line.
Since the options' keys are now different, I need to manually merge the two.
Is there a way achieve this without doing the work manually?
The trivial solution is, to not use the sections. There might be some confusion around "sections" in the options descriptions vs. sections in ini-files.
The sections in ini-files refer only to options named with embedded periods: "section1.opt1". So you can simply write the config file as:
opt1=42
# perhaps with a comment
opt2=17
See it Live On Coliru
#include <boost/program_options.hpp>
#include <fstream>
#include <iostream>
namespace po = boost::program_options;
int main(int argc, char* argv[]) {
po::options_description options;
options.add_options()
("opt1", po::value<int>(), "Option 1")
("section2.opt2", po::value<int>(), "Option 2");
std::cout << options << "\n";
po::variables_map values;
po::store(po::parse_command_line(argc, argv, options), values);
std::ifstream iniFile("options.ini");
po::store(parse_config_file(iniFile, options), values);
auto report = [&values](char const* name) {
if (auto opt = values[name]; !opt.empty())
std::cout << name << ": " << opt.as<int>() << "\n";
};
report("opt1");
report("opt2");
report("section1.opt1");
report("section2.opt2");
}
Prints
echo "opt1=42" >> options.ini; ./a.out --section2.opt2 99
--opt1 arg Option 1
--section2.opt2 arg Option 2
opt1: 42
section2.opt2: 99

How do I use a boost::program_options notifier with multiple options?

I'm writing a physics simulation that reads in a whole bunch of system parameters with the boost::program_options library and I'd like to "automatically" set some parameters that arise as combinations of the user-input values. For example, if I have
[parameters]
c0 = 299792458
dt = 0.004
as the speed of light and timestep in my input file, I'd like to set a value cdt = c0*dt in the same structure I'm using to store c0 and dt after both options get read. Notifiers, as I understand, are a way to process an input option with a function, but I haven't seen a way to do the same thing with multiple options -- what's the best way to go about doing this?
I do not see any direct option how to achieve this using boost program_options. Notifiers are being called after only one option is parsed. However you can combine several workarounds to achieve acceptable solution - store values in separate structure, let program_options to fill all computed values (directly or using setter function) after parsing is complete.
My simplified suggestion:
#include <boost/program_options.hpp>
#include <iostream>
using namespace boost::program_options;
struct SimulationConfig {
int c0;
float dt;
float cdt;
void setCdt() {
cdt = c0*dt;
}
};
int main(int argc, const char *argv[])
{
SimulationConfig config;
try
{
options_description desc{"Options"};
desc.add_options()
("help,h", "Help screen")
("c0", value<int>(&config.c0), "Speed of light")
("dt", value<float>(&config.dt), "Time interval");
variables_map vm;
store(parse_command_line(argc, argv, desc), vm);
notify(vm);
if (vm.count("help"))
std::cout << desc << '\n';
else if (vm.count("c0") && vm.count("dt")) {
//config.cdt = vm["c0"].as<int>() * vm["dt"].as<float>();
config.setCdt();
std::cout << "Cdt is set to: " << config.cdt << std::endl;
}
}
catch (const error &ex)
{
std::cerr << ex.what() << '\n';
}
}
If you use a config struct like pe3k recommends you are nearly there. Just add a notifier which calls the according setter:
options_description desc{"Options"};
desc.add_options()
("help,h", "Help screen")
("c0",
value<int>(&config.c0),
"Speed of light")
("dt",
value<float>(&config.dt)->notifier([&config](auto){ config.setCdt(); }),
"Time interval");

How to properly use boost::program_options::implicit_value for vector of string?

Proof of code:
boost::program_options::options_description options;
Parser::Parser(): options("Allowed options")
{
options.add_options()
("help,h", "produce help message")
("type,t", po::value<std::string>()->required()->implicit_value(""), "Type")
}
This line is ok:
("type,t", po::value<std::string>()->required()->implicit_value(""), "Type")
How can I add this line to work correctly?:
("file,f", po::value< std::vector<std::string> >()->required()->multitoken()->implicit_value(std::vector<std::string>(0,"")), "File(s)")
Here is vector of string-s.
You just need to help the options-description to know how to present the default value to the end user.
That is, usually implicit_value would use lexical_cast<> to get the textual representation, but that (obviously) doesn't work for vector<string>. So, supply your own textual representation:
("file,f", po::value<strings>()->required()
->implicit_value(strings { "santa", "claus" }, "santa,claus"), "File(s)");
Full Demo
Live On Coliru
#include <iostream>
#include <boost/program_options.hpp>
namespace po = boost::program_options;
int main(int argc, char** argv) {
po::options_description options/*("Allowed options")*/;
using strings = std::vector<std::string>;
options.add_options()
("help,h", "produce help message")
("file,f", po::value<strings>()->required()->implicit_value(strings { "santa", "claus" }, "santa,claus"), "File(s)");
std::cout << options << "\n";
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, options, po::command_line_style::default_style), vm);
po::notify(vm);
auto types = vm["file"].as<strings>();
for (auto t : types)
std::cout << "Got: " << t << "\n";
}
Prints:
-h [ --help ] produce help message
-f [ --file ] [=arg(=santa,claus)] File(s)
Got: santa
Got: claus

"Multiple occurrences" exception for boost program_options

I am writing the following code on boost's program_options (version 1.42). This seems straight-forward and taken pretty much as is from the tutorial. However, I get a "multiple_occurrences" error. Further investigation discovers that it's (probably) the "filename" parameter that raises it.
The parameters I am giving are:
3 1 test.txt 100
I have no insight to it whatsoever.. any help will be appreciated.
po::options_description common("Common options");
common.add_options()
("help", "produce help message")
("motif_size", po::value<int>(&motif_size), "Size of motif (subgraph)")
("prob", po::value<double>(&prob), "Probably to continue examining an edge")
("filename", po::value<string>(&input_filename), "Filename of the input graph")
("repeats", po::value<int>(&n_estimates), "Number of estimates")
;
po::options_description all;
all.add(common);
po::positional_options_description p;
p.add("motif_size", 0).add("prob", 1).add("filename", 2).add("repeats", 3);
po::variables_map vm;
po::store(po::command_line_parser(argc, argv).
options(all).positional(p).run(), vm);
po::notify(vm);
EDIT:
the second parameter to po::positional_options_description::add is the max count, not the position. The position is implied in the order you specify the positional options. So
p.add("motif_size", 0).add("prob", 1).add("filename", 2).add("repeats", 3);
should be
p.add("motif_size", 1).add("prob", 1).add("filename", 1).add("repeats", 1);
Here's a compilable snippet
include <boost/program_options.hpp>
#include <iostream>
#include <string>
int
main(unsigned argc, char** argv)
{
namespace po = boost::program_options;
po::options_description common("Common options");
common.add_options()
("help", "produce help message")
("motif_size", po::value<int>(), "Size of motif (subgraph)")
("prob", po::value<double>(), "Probably to continue examining an edge")
("filename", po::value<std::string>(), "Filename of the input graph")
("repeats", po::value<int>(), "Number of estimates")
;
po::options_description all;
all.add(common);
po::positional_options_description p;
p.add("motif_size", 1).add("prob", 1).add("filename", 1).add("repeats", 1);
po::variables_map vm;
try {
po::store(po::command_line_parser(argc, argv).
options(all).positional(p).run(), vm);
po::notify(vm);
} catch ( const boost::program_options::error& e ) {
std::cerr << e.what() << std::endl;
}
return 0;
}
and sample invocation.
macmini:~ samm$ g++ parse.cc -lboost_program_options
macmini:~ samm$ ./a.out 3 1 test.txt 100
macmini:~ samm$
My original answer is below.
What version of program_options? I had the same problem using boost 1.39, to solve it I ended up using boost 1.42.
Here's a link to the ticket describing the problem, and a patch to apply if you don't want to or can't upgrade your copy of boost. To use the new functionality, do something like this
try {
// argument parsing goes here
} catch ( const boost::program_options::multiple_occurrences& e ) {
std::cerr << e.what() << " from option: " << e.get_option_name() << std::endl;
}