diff --git a/.travis.yml b/.travis.yml index 651835a..d906b49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,21 +114,6 @@ jobs: script: - libs/$SELF/ci/cppcheck.sh - - os: linux - env: - - COMMENT=UBSAN - - B2_VARIANT=variant=debug - - TOOLSET=gcc-7 - - CXXFLAGS="cxxflags=-fno-omit-frame-pointer cxxflags=-fsanitize=undefined cxxflags=-fno-sanitize-recover=undefined" - - LINKFLAGS="linkflags=-fsanitize=undefined linkflags=-fno-sanitize-recover=undefined" - - UBSAN_OPTIONS=print_stacktrace=1 - addons: - apt: - packages: - - g++-7 - sources: - - ubuntu-toolchain-r-test - - os: linux env: - COMMENT=CodeCov @@ -154,17 +139,6 @@ jobs: # - CXXSTD=03,11 #################### Jobs to run on pushes to master, develop ################### - - # Coverity Scan - - os: linux - if: (branch IN (develop, master)) AND (type IN (cron, push)) - env: - - COMMENT="Coverity Scan" - - TOOLSET=gcc - script: - - echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- - - cd libs/$SELF - - ci/coverity.sh notifications: email: diff --git a/README.md b/README.md index 9c21fd2..b2bd595 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Distributed under the [Boost Software License, Version 1.0](http://www.boost.org ### Build Status (in progress...) -|Branch | Travis | Appveyor | Coverity Scan | codecov.io | Deps | Docs | Tests | -|:-------------: | ------ | -------- | ------------- | ---------- | ---- | ---- | ----- | -|[`master`](https://github.com/boostorg/program_options/tree/master) | [![Build Status](https://travis-ci.org/boostorg/program_options.svg?branch=master)](https://travis-ci.org/boostorg/program_options) | [![Build status](https://ci.appveyor.com/api/projects/status/upf5c528fy09fudk?svg=true)](https://ci.appveyor.com/project/jeking3/date-time-1evbf) | [![Coverity Scan Build Status](https://scan.coverity.com/projects/14908/badge.svg)](https://scan.coverity.com/projects/boostorg-program_options) | [![codecov](https://codecov.io/gh/boostorg/program_options/branch/master/graph/badge.svg)](https://codecov.io/gh/boostorg/program_options/branch/master) | [![Deps](https://img.shields.io/badge/deps-master-brightgreen.svg)](https://pdimov.github.io/boostdep-report/master/program_options.html) | [![Documentation](https://img.shields.io/badge/docs-master-brightgreen.svg)](http://www.boost.org/doc/libs/master/doc/html/program_options.html) | [![Enter the Matrix](https://img.shields.io/badge/matrix-master-brightgreen.svg)](http://www.boost.org/development/tests/master/developer/program_options.html) -|[`develop`](https://github.com/boostorg/program_options/tree/develop) | [![Build Status](https://travis-ci.org/boostorg/program_options.svg?branch=develop)](https://travis-ci.org/boostorg/program_options) | [![Build status](https://ci.appveyor.com/api/projects/status/e0quisadwh1v7ok5/branch/develop?svg=true)](https://ci.appveyor.com/project/vprus/program-options/branch/develop) | [![Coverity Scan Build Status](https://scan.coverity.com/projects/14908/badge.svg)](https://scan.coverity.com/projects/boostorg-program_options) | [![codecov](https://codecov.io/gh/boostorg/program_options/branch/develop/graph/badge.svg)](https://codecov.io/gh/boostorg/program_options/branch/develop) | [![Deps](https://img.shields.io/badge/deps-develop-brightgreen.svg)](https://pdimov.github.io/boostdep-report/develop/program_options.html) | [![Documentation](https://img.shields.io/badge/docs-develop-brightgreen.svg)](http://www.boost.org/doc/libs/develop/doc/html/program_options.html) | [![Enter the Matrix](https://img.shields.io/badge/matrix-develop-brightgreen.svg)](http://www.boost.org/development/tests/develop/developer/program_options.html) +|Branch | Travis | Appveyor | codecov.io | Deps | Docs | Tests | +|:-------------: | ------ | -------- | ---------- | ---- | ---- | ----- | +|[`master`](https://github.com/boostorg/program_options/tree/master) | [![Build Status](https://travis-ci.org/boostorg/program_options.svg?branch=master)](https://travis-ci.org/boostorg/program_options) | [![Build status](https://ci.appveyor.com/api/projects/status/e0quisadwh1v7ok5/branch/master?svg=true)](https://ci.appveyor.com/project/vprus/program-options/branch/master) | [![codecov](https://codecov.io/gh/boostorg/program_options/branch/master/graph/badge.svg)](https://codecov.io/gh/boostorg/program_options/branch/master) | [![Deps](https://img.shields.io/badge/deps-master-brightgreen.svg)](https://pdimov.github.io/boostdep-report/master/program_options.html) | [![Documentation](https://img.shields.io/badge/docs-master-brightgreen.svg)](http://www.boost.org/doc/libs/master/doc/html/program_options.html) | [![Enter the Matrix](https://img.shields.io/badge/matrix-master-brightgreen.svg)](http://www.boost.org/development/tests/master/developer/program_options.html) +|[`develop`](https://github.com/boostorg/program_options/tree/develop) | [![Build Status](https://travis-ci.org/boostorg/program_options.svg?branch=develop)](https://travis-ci.org/boostorg/program_options) | [![Build status](https://ci.appveyor.com/api/projects/status/e0quisadwh1v7ok5/branch/develop?svg=true)](https://ci.appveyor.com/project/vprus/program-options/branch/develop) | [![codecov](https://codecov.io/gh/boostorg/program_options/branch/develop/graph/badge.svg)](https://codecov.io/gh/boostorg/program_options/branch/develop) | [![Deps](https://img.shields.io/badge/deps-develop-brightgreen.svg)](https://pdimov.github.io/boostdep-report/develop/program_options.html) | [![Documentation](https://img.shields.io/badge/docs-develop-brightgreen.svg)](http://www.boost.org/doc/libs/develop/doc/html/program_options.html) | [![Enter the Matrix](https://img.shields.io/badge/matrix-develop-brightgreen.svg)](http://www.boost.org/development/tests/develop/developer/program_options.html) ### Directories @@ -34,4 +34,4 @@ Distributed under the [Boost Software License, Version 1.0](http://www.boost.org * [Ask questions](http://stackoverflow.com/questions/ask?tags=c%2B%2B,boost,boost-program_options): Be sure to read the documentation first to see if it answers your question. * [Report bugs](https://github.com/boostorg/program_options/issues): Be sure to mention Boost version, platform and compiler you're using. A small compilable code sample to reproduce the problem is always good as well. * [Submit Pull Requests](https://github.com/boostorg/program_options/pulls) against the **develop** branch. Note that by submitting patches you agree to license your modifications under the [Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt). Be sure to include tests proving your changes work properly. -* Discussions about the library are held on the [Boost developers mailing list](http://www.boost.org/community/groups.html#main). Be sure to read the [discussion policy](http://www.boost.org/community/policy.html) before posting and add the `[date_time]` tag at the beginning of the subject line. +* Discussions about the library are held on the [Boost developers mailing list](http://www.boost.org/community/groups.html#main). Be sure to read the [discussion policy](http://www.boost.org/community/policy.html) before posting and add the `[program_options]` tag at the beginning of the subject line. diff --git a/doc/overview.xml b/doc/overview.xml index f7de296..52fe9b9 100644 --- a/doc/overview.xml +++ b/doc/overview.xml @@ -512,7 +512,13 @@ visual_bell=yes gui.accessibility.visual_bell=yes - + When the option "gui.accessibility.visual_bell" has been added to the options + +options_description desc; +desc.add_options() + ("gui.accessibility.visual_bell", value<string>(), "flash screen for bell") + ; +
@@ -559,12 +565,49 @@ gui.accessibility.visual_bell=yes function, any function taking a std::string and returning std::string. That function will be called for each environment variable and should return either the name of the option, or - empty string if the variable should be ignored. + empty string if the variable should be ignored. An example showing this + method can be found in "example/env_options.cpp".
+
+ Types + + Everything that is passed in on the command line, as an environmental + variable, or in a config file is a string. For values that need to be used + as a non-string type, the value in the variables_map will attempt to + convert it to the correct type. + + Integers and floating point values are converted using Boost's + lexical_cast. It will accept integer values such as "41" or "-42". It will + accept floating point numbers such as "51.1", "-52.1", "53.1234567890" (as + a double), "54", "55.", ".56", "57.1e5", "58.1E5", ".591e5", "60.1e-5", + "-61.1e5", "-62.1e-5", etc. Unfortunately, hex, octal, and binary + representations that are available in C++ literals are not supported by + lexical_cast, and thus will not work with program_options. + + Booleans a special in that there are multiple ways to come at them. + Similar to another value type, it can be specified as ("my-option", + value<bool>()), and then set as: + +example --my-option=true + + However, more typical is that boolean values are set by the simple + presence of a switch. This is enabled by &bool_switch; as in + ("other-option", bool_switch()). This will cause the value to + default to false and it will become true if the switch is found: + +example --other-switch + + When a boolean does take a parameter, there are several options. + Those that evaluate to true in C++ are: "true", "yes", "on", "1". Those + that evaluate to false in C++ are: "false", "no", "off", "0". In addition, + when reading from a config file, the option name with an equal sign and no + value after it will also evaluate to true. +
+
Annotated List of Symbols @@ -649,4 +692,4 @@ gui.accessibility.visual_bell=yes sgml-parent-document: ("program_options.xml" "section") sgml-set-face: t End: ---> \ No newline at end of file +--> diff --git a/doc/program_options.ent b/doc/program_options.ent index 084f605..cd6504b 100644 --- a/doc/program_options.ent +++ b/doc/program_options.ent @@ -44,3 +44,5 @@ basic_option"> +bool_switch"> diff --git a/doc/tutorial.xml b/doc/tutorial.xml index d3e447a..b937576 100644 --- a/doc/tutorial.xml +++ b/doc/tutorial.xml @@ -208,9 +208,9 @@ Allowed options: --input-file arg : input file $ bin/gcc/debug/options_description Optimization level is 10 -$ bin/gcc/debug/options_description --optimization 4 -I foo a.cpp -Include paths are: foo -Input files are: a.cpp +$ bin/gcc/debug/options_description --optimization 4 -I foo -I another/path --include-path third/include/path a.cpp b.cpp +Include paths are: foo another/path third/include/path +Input files are: a.cpp b.cpp Optimization level is 4 @@ -350,4 +350,4 @@ Optimization level is 4 sgml-parent-document: ("program_options.xml" "section") sgml-set-face: t End: ---> \ No newline at end of file +--> diff --git a/example/Jamfile.v2 b/example/Jamfile.v2 index d669d3e..9f0b1d8 100644 --- a/example/Jamfile.v2 +++ b/example/Jamfile.v2 @@ -12,3 +12,10 @@ exe custom_syntax : custom_syntax.cpp ; exe real : real.cpp ; exe regex : regex.cpp /boost/regex//boost_regex ; + +# The following examples use C++ features beyond C++03. +# It would be possible to make compilation of each conditional on specific config check, +# for now just disable the compilation. +#exe config_file_types : config_file_types.cpp ; +#exe env_options : env_options.cpp ; +#exe options_heirarchy : options_heirarchy.cpp ; diff --git a/example/config_file_types.cpp b/example/config_file_types.cpp new file mode 100644 index 0000000..e466e94 --- /dev/null +++ b/example/config_file_types.cpp @@ -0,0 +1,242 @@ +// Copyright Thomas Kent 2016 +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// This example shows a config file (in ini format) being parsed by the +// program_options library. It includes a numebr of different value types. + +#include +namespace po = boost::program_options; + +#include +#include +#include +using namespace std; + +const double FLOAT_SEPERATION = 0.00000000001; +bool check_float(double test, double expected) +{ + double seperation = expected * (1 + FLOAT_SEPERATION) / expected; + if ((test < expected + seperation) && (test > expected - seperation)) + { + return true; + } + return false; +} + +stringstream make_file() +{ + stringstream ss; + ss << "# This file checks parsing of various types of config values\n"; + //FAILS: ss << "; a windows style comment\n"; + + ss << "global_string = global value\n"; + ss << "unregistered_entry = unregistered value\n"; + + ss << "\n[strings]\n"; + ss << "word = word\n"; + ss << "phrase = this is a phrase\n"; + ss << "quoted = \"quotes are in result\"\n"; + + ss << "\n[ints]\n"; + ss << "positive = 41\n"; + ss << "negative = -42\n"; + //FAILS: Lexical cast doesn't support hex, oct, or bin + //ss << "hex = 0x43\n"; + //ss << "oct = 044\n"; + //ss << "bin = 0b101010\n"; + + ss << "\n[floats]\n"; + ss << "positive = 51.1\n"; + ss << "negative = -52.1\n"; + ss << "double = 53.1234567890\n"; + ss << "int = 54\n"; + ss << "int_dot = 55.\n"; + ss << "dot = .56\n"; + ss << "exp_lower = 57.1e5\n"; + ss << "exp_upper = 58.1E5\n"; + ss << "exp_decimal = .591e5\n"; + ss << "exp_negative = 60.1e-5\n"; + ss << "exp_negative_val = -61.1e5\n"; + ss << "exp_negative_negative_val = -62.1e-5\n"; + + ss << "\n[booleans]\n"; + ss << "number_true = 1\n"; + ss << "number_false = 0\n"; + ss << "yn_true = yes\n"; + ss << "yn_false = no\n"; + ss << "tf_true = true\n"; + ss << "tf_false = false\n"; + ss << "onoff_true = on\n"; + ss << "onoff_false = off\n"; + ss << "present_equal_true = \n"; + //FAILS: Must be an = + //ss << "present_no_equal_true\n"; + + ss.seekp(ios_base::beg); + return ss; +} + +po::options_description set_options() +{ + po::options_description opts; + opts.add_options() + ("global_string", po::value()) + + ("strings.word", po::value()) + ("strings.phrase", po::value()) + ("strings.quoted", po::value()) + + ("ints.positive", po::value()) + ("ints.negative", po::value()) + ("ints.hex", po::value()) + ("ints.oct", po::value()) + ("ints.bin", po::value()) + + ("floats.positive", po::value()) + ("floats.negative", po::value()) + ("floats.double", po::value()) + ("floats.int", po::value()) + ("floats.int_dot", po::value()) + ("floats.dot", po::value()) + ("floats.exp_lower", po::value()) + ("floats.exp_upper", po::value()) + ("floats.exp_decimal", po::value()) + ("floats.exp_negative", po::value()) + ("floats.exp_negative_val", po::value()) + ("floats.exp_negative_negative_val", po::value()) + + // Load booleans as value, so they will require a --option=value on the command line + //("booleans.number_true", po::value()) + //("booleans.number_false", po::value()) + //("booleans.yn_true", po::value()) + //("booleans.yn_false", po::value()) + //("booleans.tf_true", po::value()) + //("booleans.tf_false", po::value()) + //("booleans.onoff_true", po::value()) + //("booleans.onoff_false", po::value()) + //("booleans.present_equal_true", po::value()) + //("booleans.present_no_equal_true", po::value()) + + // Load booleans as bool_switch, so that a --option will set it true on the command line + // The difference between these two types does not show up when parsing a file + ("booleans.number_true", po::bool_switch()) + ("booleans.number_false", po::bool_switch()) + ("booleans.yn_true", po::bool_switch()) + ("booleans.yn_false", po::bool_switch()) + ("booleans.tf_true", po::bool_switch()) + ("booleans.tf_false", po::bool_switch()) + ("booleans.onoff_true", po::bool_switch()) + ("booleans.onoff_false", po::bool_switch()) + ("booleans.present_equal_true", po::bool_switch()) + ("booleans.present_no_equal_true", po::bool_switch()) + ; + return opts; +} + +vector parse_file(stringstream &file, po::options_description &opts, po::variables_map &vm) +{ + const bool ALLOW_UNREGISTERED = true; + cout << file.str() << endl; + + po::parsed_options parsed = parse_config_file(file, opts, ALLOW_UNREGISTERED); + store(parsed, vm); + vector unregistered = po::collect_unrecognized(parsed.options, po::exclude_positional); + notify(vm); + + return unregistered; +} + +void check_results(po::variables_map &vm, vector unregistered) +{ + // Check that we got the correct values back + string expected_global_string = "global value"; + + string expected_unreg_option = "unregistered_entry"; + string expected_unreg_value = "unregistered value"; + + string expected_strings_word = "word"; + string expected_strings_phrase = "this is a phrase"; + string expected_strings_quoted = "\"quotes are in result\""; + + int expected_int_postitive = 41; + int expected_int_negative = -42; + int expected_int_hex = 0x43; + int expected_int_oct = 044; + int expected_int_bin = 0b101010; + + float expected_float_positive = 51.1f; + float expected_float_negative = -52.1f; + double expected_float_double = 53.1234567890; + float expected_float_int = 54.0f; + float expected_float_int_dot = 55.0f; + float expected_float_dot = .56f; + float expected_float_exp_lower = 57.1e5f; + float expected_float_exp_upper = 58.1E5f; + float expected_float_exp_decimal = .591e5f; + float expected_float_exp_negative = 60.1e-5f; + float expected_float_exp_negative_val = -61.1e5f; + float expected_float_exp_negative_negative_val = -62.1e-5f; + + bool expected_number_true = true; + bool expected_number_false = false; + bool expected_yn_true = true; + bool expected_yn_false = false; + bool expected_tf_true = true; + bool expected_tf_false = false; + bool expected_onoff_true = true; + bool expected_onoff_false = false; + bool expected_present_equal_true = true; + bool expected_present_no_equal_true = true; + + assert(vm["global_string"].as() == expected_global_string); + + assert(unregistered[0] == expected_unreg_option); + assert(unregistered[1] == expected_unreg_value); + + assert(vm["strings.word"].as() == expected_strings_word); + assert(vm["strings.phrase"].as() == expected_strings_phrase); + assert(vm["strings.quoted"].as() == expected_strings_quoted); + + assert(vm["ints.positive"].as() == expected_int_postitive); + assert(vm["ints.negative"].as() == expected_int_negative); + //assert(vm["ints.hex"].as() == expected_int_hex); + //assert(vm["ints.oct"].as() == expected_int_oct); + //assert(vm["ints.bin"].as() == expected_int_bin); + + assert(check_float(vm["floats.positive"].as(), expected_float_positive)); + assert(check_float(vm["floats.negative"].as(), expected_float_negative)); + assert(check_float(vm["floats.double"].as(), expected_float_double)); + assert(check_float(vm["floats.int"].as(), expected_float_int)); + assert(check_float(vm["floats.int_dot"].as(), expected_float_int_dot)); + assert(check_float(vm["floats.dot"].as(), expected_float_dot)); + assert(check_float(vm["floats.exp_lower"].as(), expected_float_exp_lower)); + assert(check_float(vm["floats.exp_upper"].as(), expected_float_exp_upper)); + assert(check_float(vm["floats.exp_decimal"].as(), expected_float_exp_decimal)); + assert(check_float(vm["floats.exp_negative"].as(), expected_float_exp_negative)); + assert(check_float(vm["floats.exp_negative_val"].as(), expected_float_exp_negative_val)); + assert(check_float(vm["floats.exp_negative_negative_val"].as(), expected_float_exp_negative_negative_val)); + + assert(vm["booleans.number_true"].as() == expected_number_true); + assert(vm["booleans.number_false"].as() == expected_number_false); + assert(vm["booleans.yn_true"].as() == expected_yn_true); + assert(vm["booleans.yn_false"].as() == expected_yn_false); + assert(vm["booleans.tf_true"].as() == expected_tf_true); + assert(vm["booleans.tf_false"].as() == expected_tf_false); + assert(vm["booleans.onoff_true"].as() == expected_onoff_true); + assert(vm["booleans.onoff_false"].as() == expected_onoff_false); + assert(vm["booleans.present_equal_true"].as() == expected_present_equal_true); + //assert(vm["booleans.present_no_equal_true"].as() == expected_present_no_equal_true); +} + +int main(int ac, char* av[]) +{ + auto file = make_file(); + auto opts = set_options(); + po::variables_map vars; + auto unregistered = parse_file(file, opts, vars); + check_results(vars, unregistered); + + return 0; +} diff --git a/example/env_options.cpp b/example/env_options.cpp new file mode 100644 index 0000000..bab37e2 --- /dev/null +++ b/example/env_options.cpp @@ -0,0 +1,47 @@ +// Copyright Thomas Kent 2016 +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +namespace po = boost::program_options; +#include +#include + +std::string mapper(std::string env_var) +{ + // ensure the env_var is all caps + std::transform(env_var.begin(), env_var.end(), env_var.begin(), ::toupper); + + if (env_var == "PATH") return "path"; + if (env_var == "EXAMPLE_VERBOSE") return "verbosity"; + return ""; +} + +void get_env_options() +{ + po::options_description config("Configuration"); + config.add_options() + ("path", "the execution path") + ("verbosity", po::value()->default_value("INFO"), "set verbosity: DEBUG, INFO, WARN, ERROR, FATAL") + ; + + po::variables_map vm; + store(po::parse_environment(config, boost::function1(mapper)), vm); + notify(vm); + + if (vm.count("path")) + { + std::cout << "First 75 chars of the system path: \n"; + std::cout << vm["path"].as().substr(0, 75) << std::endl; + } + + std::cout << "Verbosity: " << vm["verbosity"].as() << std::endl; +} + +int main(int ac, char* av[]) +{ + get_env_options(); + + return 0; +} diff --git a/example/options_heirarchy.cpp b/example/options_heirarchy.cpp new file mode 100644 index 0000000..c913b14 --- /dev/null +++ b/example/options_heirarchy.cpp @@ -0,0 +1,690 @@ +// Copyright Thomas Kent 2016 +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// +// This is an example of a program that uses multiple facets of the boost +// program_options library. It will go through different types of config +// options in a heirarchal manner: +// 1. Default options are set. +// 2. Command line options are set (they override defaults). +// 3. Environment options are set (they override defaults but not command +// line options). +// 4. Config files specified on the command line are read, if present, in +// the order specified. (these override defaults but not options from the +// other steps). +// 5. Default config file (default.cfg) is read, if present (it overrides +// defaults but not options from the other steps). +// +// See the bottom of this file for full usage examples +// + +#include +namespace po = boost::program_options; +#include +#include +#include +#include +#include + +const std::string version("1.0"); + +// Used to exit the program if the help/version option is set +class OptionsExitsProgram : public std::exception +{}; + +struct GuiOpts +{ + unsigned int width; + unsigned int height; +}; + +struct NetworkOpts +{ + std::string address; + unsigned short port; +}; + +class OptionsHeirarchy +{ +public: + // The constructor sets up all the various options that will be parsed + OptionsHeirarchy() + { + SetOptions(); + } + + // Parse options runs through the heirarchy doing all the parsing + void ParseOptions(int argc, char* argv[]) + { + ParseCommandLine(argc, argv); + CheckForHelp(); + CheckForVersion(); + ParseEnvironment(); + ParseConfigFiles(); + ParseDefaultConfigFile(); + } + + // Below is the interface to access the data, once ParseOptions has been run + std::string Path() + { + return results["path"].as(); + } + std::string Verbosity() + { + return results["verbosity"].as(); + } + std::vector IncludePath() + { + if (results.count("include-path")) + { + return results["include-path"].as>(); + } + return std::vector(); + } + std::string MasterFile() + { + if (results.count("master-file")) + { + return results["master-file"].as(); + } + return ""; + } + std::vector Files() + { + if (results.count("file")) + { + return results["file"].as>(); + } + return std::vector(); + } + bool GUI() + { + if (results["run-gui"].as()) + { + return true; + } + return false; + } + GuiOpts GuiValues() + { + GuiOpts opts; + opts.width = results["gui.width"].as(); + opts.height = results["gui.height"].as(); + return opts; + } + NetworkOpts NetworkValues() + { + NetworkOpts opts; + opts.address = results["network.ip"].as(); + opts.port = results["network.port"].as(); + return opts; + } + +private: + void SetOptions() + { + SetCommandLineOptions(); + SetCommonOptions(); + SetConfigOnlyOptions(); + SetEnvMapping(); + } + void SetCommandLineOptions() + { + command_line_options.add_options() + ("help,h", "display this help message") + ("version,v", "show program version") + ("config,c", po::value>(), + "config files to parse (always parses default.cfg)") + ; + hidden_command_line_options.add_options() + ("master-file", po::value()) + ("file", po::value>()) + ; + positional_options.add("master-file", 1); + positional_options.add("file", -1); + } + void SetCommonOptions() + { + common_options.add_options() + ("path", po::value()->default_value(""), + "the execution path to use (imports from environment if not specified)") + ("verbosity", po::value()->default_value("INFO"), + "set verbosity: DEBUG, INFO, WARN, ERROR, FATAL") + ("include-path,I", po::value>()->composing(), + "paths to search for include files") + ("run-gui", po::bool_switch(), "start the GUI") + ; + } + void SetConfigOnlyOptions() + { + config_only_options.add_options() + ("log-dir", po::value()->default_value("log")) + ("gui.height", po::value()->default_value(100)) + ("gui.width", po::value()->default_value(100)) + ("network.ip", po::value()->default_value("127.0.0.1")) + ("network.port", po::value()->default_value(12345)) + ; + // Run a parser here (with no command line options) to add these defaults into + // results, this way they will be enabled even if no config files are parsed. + store(po::command_line_parser(0, 0).options(config_only_options).run(), results); + notify(results); + } + void SetEnvMapping() + { + env_to_option["PATH"] = "path"; + env_to_option["EXAMPLE_VERBOSE"] = "verbosity"; + } + + + void ParseCommandLine(int argc, char* argv[]) + { + po::options_description cmd_opts; + cmd_opts.add(command_line_options).add(hidden_command_line_options).add(common_options); + store(po::command_line_parser(argc, argv). + options(cmd_opts).positional(positional_options).run(), results); + notify(results); + } + void CheckForHelp() + { + if (results.count("help")) + { + PrintHelp(); + } + } + void PrintHelp() + { + std::cout << "Program Options Example" << std::endl; + std::cout << "Usage: example [OPTION]... MASTER-FILE [FILE]...\n"; + std::cout << " or example [OPTION] --run-gui\n"; + po::options_description help_opts; + help_opts.add(command_line_options).add(common_options); + std::cout << help_opts << std::endl; + throw OptionsExitsProgram(); + } + void CheckForVersion() + { + if (results.count("version")) + { + PrintVersion(); + } + } + void PrintVersion() + { + std::cout << "Program Options Example " << version << std::endl; + throw OptionsExitsProgram(); + } + void ParseEnvironment() + { + store(po::parse_environment(common_options, + // The next two lines are the crazy syntax to use EnvironmentMapper as + // the lookup function for env->config name conversions + boost::function1( + std::bind1st(std::mem_fun(&OptionsHeirarchy::EnvironmentMapper), this))), + results); + notify(results); + } + std::string EnvironmentMapper(std::string env_var) + { + // ensure the env_var is all caps + std::transform(env_var.begin(), env_var.end(), env_var.begin(), ::toupper); + + auto entry = env_to_option.find(env_var); + if (entry != env_to_option.end()) + { + return entry->second; + } + return ""; + } + void ParseConfigFiles() + { + if (results.count("config")) + { + auto files = results["config"].as>(); + for (auto file = files.begin(); file != files.end(); file++) + { + LoadAConfigFile(*file); + } + } + } + void LoadAConfigFile(std::string filename) + { + bool ALLOW_UNREGISTERED = true; + + po::options_description config_opts; + config_opts.add(config_only_options).add(common_options); + + std::ifstream cfg_file(filename.c_str()); + if (cfg_file) + { + store(parse_config_file(cfg_file, config_opts, ALLOW_UNREGISTERED), results); + notify(results); + } + } + void ParseDefaultConfigFile() + { + LoadAConfigFile("default.cfg"); + } + + std::map env_to_option; + po::options_description config_only_options; + po::options_description common_options; + po::options_description command_line_options; + po::options_description hidden_command_line_options; + po::positional_options_description positional_options; + po::variables_map results; +}; + + +void get_env_options() +{ +} + +void PrintOptions(OptionsHeirarchy options) +{ + auto path = options.Path(); + if (path.length()) + { + std::cout << "First 75 chars of the system path: \n"; + std::cout << options.Path().substr(0, 75) << std::endl; + } + + std::cout << "Verbosity: " << options.Verbosity() << std::endl; + std::cout << "Include Path:\n"; + auto includePaths = options.IncludePath(); + for (auto path = includePaths.begin(); path != includePaths.end(); path++) + { + std::cout << " " << *path << std::endl; + } + std::cout << "Master-File: " << options.MasterFile() << std::endl; + std::cout << "Additional Files:\n"; + auto files = options.Files(); + for (auto file = files.begin(); file != files.end(); file++) + { + std::cout << " " << *file << std::endl; + } + + std::cout << "GUI Enabled: " << std::boolalpha << options.GUI() << std::endl; + if (options.GUI()) + { + auto gui_values = options.GuiValues(); + std::cout << "GUI Height: " << gui_values.height << std::endl; + std::cout << "GUI Width: " << gui_values.width << std::endl; + } + + auto network_values = options.NetworkValues(); + std::cout << "Network Address: " << network_values.address << std::endl; + std::cout << "Network Port: " << network_values.port << std::endl; +} + +int main(int ac, char* av[]) +{ + OptionsHeirarchy options; + try + { + options.ParseOptions(ac, av); + PrintOptions(options); + } + catch (OptionsExitsProgram){} + + return 0; +} + +/* +Full Usage Examples +=================== + +These were run on windows, so some results may show that environment, but +results should be similar on POSIX platforms. + +Help +---- +To see the help screen, with the available options just pass the --help (or -h) +parameter. The program will then exit. + + > example.exe --help + Program Options Example + Usage: example [OPTION]... MASTER-FILE [FILE]... + or example [OPTION] --run-gui + + -h [ --help ] display this help message + -v [ --version ] show program version + -c [ --config ] arg config files to parse (always parses default.cfg) + + --path arg the execution path to use (imports from + environment if not specified) + --verbosity arg (=INFO) set verbosity: DEBUG, INFO, WARN, ERROR, FATAL + -I [ --include-path ] arg paths to search for include files + --run-gui start the GUI + +Version is similar to help (--version or -v). + + > example.exe -v + Program Options Example 1.0 + +Basics +------ +Running without any options will get the default values (path is set from the +environment): + + > example.exe + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +We can easily override that environment path with a simple option: + + > example.exe --path a/b/c;d/e/f + First 75 chars of the system path: + a/b/c;d/e/f + Verbosity: INFO + Include Path: + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +You can use a space or equals sign after long options, also backslashes are +treated literally on windows, on POSIX they need to be escaped. + + > example.exe --path=a\b\c\;d\e\\f + First 75 chars of the system path: + a\b\c\;d\e\\f + Verbosity: INFO + Include Path: + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +For short options you can use a space: + + > example.exe -I path/to/includes + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + path\to\includes + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +Or you can put the option immediately after it: + + > example.exe -Ipath/to/includes + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + path\to\includes + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +The include path (--include-path or -I) option allows for multiple paths to be +specified (both on the command line and in config files) and combined into a +vector for use by the program. + + > example.exe --include-path=a/b/c --include-path d/e/f -I g/h/i -Ij/k/l + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + a/b/c + d/e/f + g/h/i + j/k/l + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +There are also the option of flags that do not take parameters and just set a +boolean value to true. In this case, running the gui also causes default values +for the gui to be output to the screen. + + > example.exe --run-gui + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + Master-File: + Additional Files: + GUI Enabled: true + GUI Height: 100 + GUI Width: 100 + Network Address: 127.0.0.1 + Network Port: 12345 + +There are also "positional" options at the end of the command line. The first +one specifies the "master" file the others are additional files. + + > example.exe --path=a-path -I an-include master.cpp additional1.cpp additional2.cpp + First 75 chars of the system path: + a-path + Verbosity: INFO + Include Path: + an-include + Master-File: master.cpp + Additional Files: + additional1.cpp + additional2.cpp + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +Environment Variables +--------------------- +In addition to the PATH environment variable, it also knows how to read the +EXAMPLE_VERBOSE environmental variable and use that to set the verbosity +option/ + + > set EXAMPLE_VERBOSE=DEBUG + > example.exe + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: DEBUG + Include Path: + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +However, if the --verboseity flag is also set, it will override the env +variable. This illustrates an important example, the way program_options works, +is that a parser will not override a value that has previously been set by +another parser. Thus the env parser doesn't override the command line parser. +(We will see this again in config files.) Default values are seperate from this +heirarcy, they only apply if no parser has set the value and it is being read. + + > set EXAMPLE_VERBOSE=DEBUG + > example.exe --verbosity=WARN + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: WARN + Include Path: + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +(You can unset an environmental variable with an empty set command) + + > set EXAMPLE_VERBOSE= + > example.exe + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + + +Config Files +------------ +Config files generally follow the [INI file format] +(https://en.wikipedia.org/wiki/INI_file) with a few exceptions. + +Values can be simply added tp options with an equal sign. Here are two include +paths added via the default config file (default.cfg), you can have optional +spaces around the equal sign. + + # You can use comments in a config file + include-path=first/default/path + include-path = second/default/path + +Results in + + > example.exe + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + first/default/path + second/default/path + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 127.0.0.1 + Network Port: 12345 + +Values can also be in sections of the config file. Again, editing default.cfg + + include-path=first/default/path + include-path = second/default/path + + [network] + ip=1.2.3.4 + port=3000 + +Results in + + > example.exe + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: INFO + Include Path: + first/default/path + second/default/path + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 1.2.3.4 + Network Port: 3000 + +This example is also setup to allow multiple config files to be specified on +the command line, which are checked before the default.cfg file is read (but +after the environment and command line parsing). Thus we can set the first.cfg +file to contain the following: + + verbosity=ERROR + + [network] + ip = 5.6.7.8 + +Results in: + + > example.exe --config first.cfg + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: ERROR + Include Path: + first/default/path + second/default/path + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 5.6.7.8 + Network Port: 3000 + +But since the config files are read after the command line, setting the +verbosity there causes the value in the file to be ignored. + + > example.exe --config first.cfg --verbosity=WARN + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: WARN + Include Path: + first/default/path + second/default/path + Master-File: + Additional Files: + GUI Enabled: false + Network Address: 5.6.7.8 + Network Port: 3000 + +The config files are parsed in the order they are received on the command line. +So adding the second.cfg file: + + verbosity=FATAL + run-gui=true + + [gui] + height=720 + width=1280 + +Results in a combination of all three config files: + + > example.exe --config first.cfg --config second.cfg + First 75 chars of the system path: + C:\Program Files (x86)\MSBuild\14.0\bin;C:\Perl\site\bin;C:\Perl\bin;C:\Pro + Verbosity: ERROR + Include Path: + first/default/path + second/default/path + Master-File: + Additional Files: + GUI Enabled: true + GUI Height: 720 + GUI Width: 1280 + Network Address: 5.6.7.8 + Network Port: 3000 + +Incidently the boolean run-gui option could have been set a number of ways +that all result in the C++ boolean value of true: + + run-gui=true + run-gui=on + run-gui=1 + run-gui=yes + run-gui= + +Since run-gui is an option that was set with the bool_switch type, which +forces its use on the command line without a parameter (i.e. --run-gui instead +of --run-gui=true) it can't be given a "false" option, bool_switch values can +only be turned true. If instead we had a value ("my-switch", po::value()) +that could be set at the command line --my-switch=true or --my-switch=false, or +any of the other types of boolean keywords true: true, on, 1, yes; +false: false, off, 0, no. In a config file this could look like: + + my-switch=true + my-switch=on + my-switch=1 + my-switch=yes + my-switch= + + my-switch=false + my-switch=off + my-switch=0 + my-switch=no + +*/ diff --git a/include/boost/program_options/errors.hpp b/include/boost/program_options/errors.hpp index ba83d88..ba0f404 100644 --- a/include/boost/program_options/errors.hpp +++ b/include/boost/program_options/errors.hpp @@ -375,12 +375,15 @@ namespace boost { namespace program_options { const std::string& option_name = "", const std::string& original_token = "", int option_style = 0): - error_with_option_name(get_template(kind), option_name, original_token, option_style) + error_with_option_name(get_template(kind), option_name, original_token, option_style), + m_kind(kind) { } ~validation_error() throw() {} + kind_t kind() const { return m_kind; } + protected: /** Used to convert kind_t to a related error text */ std::string get_template(kind_t kind); diff --git a/include/boost/program_options/options_description.hpp b/include/boost/program_options/options_description.hpp index fac6acc..90d913d 100644 --- a/include/boost/program_options/options_description.hpp +++ b/include/boost/program_options/options_description.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -106,14 +107,16 @@ namespace program_options { /** Returns the canonical name for the option description to enable the user to recognised a matching option. 1) For short options ('-', '/'), returns the short name prefixed. - 2) For long options ('--' / '-') returns the long name prefixed - 3) All other cases, returns the long name (if present) or the short name, - unprefixed. + 2) For long options ('--' / '-') returns the first long name prefixed + 3) All other cases, returns the first long name (if present) or the short + name, unprefixed. */ std::string canonical_display_name(int canonical_option_style = 0) const; const std::string& long_name() const; + const std::pair long_names() const; + /// Explanation of this option const std::string& description() const; @@ -129,9 +132,24 @@ namespace program_options { private: - option_description& set_name(const char* name); + option_description& set_names(const char* name); + + /** + * a one-character "switch" name - with its prefix, + * so that this is either empty or has length 2 (e.g. "-c" + */ + std::string m_short_name; + + /** + * one or more names by which this option may be specified + * on a command-line or in a config file, which are not + * a single-letter switch. The names here are _without_ + * any prefix. + */ + std::vector m_long_names; + + std::string m_description; - std::string m_short_name, m_long_name, m_description; // shared_ptr is needed to simplify memory management in // copy ctor and destructor. shared_ptr m_value_semantic; diff --git a/src/options_description.cpp b/src/options_description.cpp index 6592a5d..d4fcfc8 100644 --- a/src/options_description.cpp +++ b/src/options_description.cpp @@ -49,21 +49,21 @@ namespace boost { namespace program_options { } option_description:: - option_description(const char* name, + option_description(const char* names, const value_semantic* s) : m_value_semantic(s) { - this->set_name(name); + this->set_names(names); } option_description:: - option_description(const char* name, + option_description(const char* names, const value_semantic* s, const char* description) : m_description(description), m_value_semantic(s) { - this->set_name(name); + this->set_names(names); } option_description::~option_description() @@ -77,38 +77,42 @@ namespace boost { namespace program_options { bool short_ignore_case) const { match_result result = no_match; + std::string local_option = (long_ignore_case ? tolower_(option) : option); - std::string local_long_name((long_ignore_case ? tolower_(m_long_name) : m_long_name)); - - if (!local_long_name.empty()) { - - std::string local_option = (long_ignore_case ? tolower_(option) : option); + for(std::vector::const_iterator it(m_long_names.begin()); it != m_long_names.end(); it++) + { + std::string local_long_name((long_ignore_case ? tolower_(*it) : *it)); - if (*local_long_name.rbegin() == '*') - { - // The name ends with '*'. Any specified name with the given - // prefix is OK. - if (local_option.find(local_long_name.substr(0, local_long_name.length()-1)) - == 0) - result = approximate_match; - } + if (!local_long_name.empty()) { - if (local_long_name == local_option) - { - result = full_match; - } - else if (approx) - { - if (local_long_name.find(local_option) == 0) + + if ((result == no_match) && (*local_long_name.rbegin() == '*')) { - result = approximate_match; + // The name ends with '*'. Any specified name with the given + // prefix is OK. + if (local_option.find(local_long_name.substr(0, local_long_name.length()-1)) + == 0) + result = approximate_match; + } + + if (local_long_name == local_option) + { + result = full_match; + break; + } + else if (approx) + { + if (local_long_name.find(local_option) == 0) + { + result = approximate_match; + } } } + } - + if (result != full_match) { - std::string local_option(short_ignore_case ? tolower_(option) : option); std::string local_short_name(short_ignore_case ? tolower_(m_short_name) : m_short_name); if (local_short_name == local_option) @@ -122,9 +126,12 @@ namespace boost { namespace program_options { const std::string& option_description::key(const std::string& option) const - { - if (!m_long_name.empty()) - if (m_long_name.find('*') != string::npos) + { + // We make the arbitrary choise of using the first long + // name as the key, regardless of anything else + if (!m_long_names.empty()) { + const std::string& first_long_name = *m_long_names.begin(); + if (first_long_name.find('*') != string::npos) // The '*' character means we're long_name // matches only part of the input. So, returning // long name will remove some of the information, @@ -132,7 +139,8 @@ namespace boost { namespace program_options { // in the source. return option; else - return m_long_name; + return first_long_name; + } else return m_short_name; } @@ -140,12 +148,13 @@ namespace boost { namespace program_options { std::string option_description::canonical_display_name(int prefix_style) const { - if (!m_long_name.empty()) + // We prefer the first long name over any others + if (!m_long_names.empty()) { if (prefix_style == command_line_style::allow_long) - return "--" + m_long_name; + return "--" + *m_long_names.begin(); if (prefix_style == command_line_style::allow_long_disguise) - return "-" + m_long_name; + return "-" + *m_long_names.begin(); } // sanity check: m_short_name[0] should be '-' or '/' if (m_short_name.length() == 2) @@ -155,8 +164,8 @@ namespace boost { namespace program_options { if (prefix_style == command_line_style::allow_dash_for_short) return string("-") + m_short_name[1]; } - if (!m_long_name.empty()) - return m_long_name; + if (!m_long_names.empty()) + return *m_long_names.begin(); else return m_short_name; } @@ -165,21 +174,46 @@ namespace boost { namespace program_options { const std::string& option_description::long_name() const { - return m_long_name; + static std::string empty_string(""); + return m_long_names.empty() ? empty_string : *m_long_names.begin(); + } + + const std::pair + option_description::long_names() const + { + return (m_long_names.empty()) + ? std::pair( NULL, 0 ) + : std::pair( &(*m_long_names.begin()), m_long_names.size()); } option_description& - option_description::set_name(const char* _name) + option_description::set_names(const char* _names) { - std::string name(_name); - string::size_type n = name.find(','); - if (n != string::npos) { - assert(n == name.size()-2); - m_long_name = name.substr(0, n); - m_short_name = '-' + name.substr(n+1,1); - } else { - m_long_name = name; + m_long_names.clear(); + std::istringstream iss(_names); + std::string name; + + while(std::getline(iss, name, ',')) { + m_long_names.push_back(name); } + assert(!m_long_names.empty() && "No option names were specified"); + + bool try_interpreting_last_name_as_a_switch = m_long_names.size() > 1; + if (try_interpreting_last_name_as_a_switch) { + const std::string& last_name = *m_long_names.rbegin(); + if (last_name.length() == 1) { + m_short_name = '-' + last_name; + m_long_names.pop_back(); + // The following caters to the (valid) input of ",c" for some + // character c, where the caller only wants this option to have + // a short name. + if (m_long_names.size() == 1 && (*m_long_names.begin()).empty()) { + m_long_names.clear(); + } + } + } + // We could theoretically also ensure no remaining long names + // are empty, or that none of them have length 1 return *this; } @@ -200,12 +234,12 @@ namespace boost { namespace program_options { { if (!m_short_name.empty()) { - return m_long_name.empty() + return m_long_names.empty() ? m_short_name : string(m_short_name).append(" [ --"). - append(m_long_name).append(" ]"); + append(*m_long_names.begin()).append(" ]"); } - return string("--").append(m_long_name); + return string("--").append(*m_long_names.begin()); } std::string diff --git a/test/cmdline_test.cpp b/test/cmdline_test.cpp index e971819..1fc0af8 100644 --- a/test/cmdline_test.cpp +++ b/test/cmdline_test.cpp @@ -463,11 +463,13 @@ void test_additional_parser() desc.add_options() ("response-file", value(), "response file") ("foo", value(), "foo") + ("bar,baz", value(), "bar") ; vector input; input.push_back("@config"); input.push_back("--foo=1"); + input.push_back("--baz=11"); cmdline cmd(input); cmd.set_options_description(desc); @@ -475,11 +477,13 @@ void test_additional_parser() vector