From 88a76d4b01fa1043435b05786338441df771546b Mon Sep 17 00:00:00 2001 From: Marek Roszko Date: Thu, 31 Aug 2023 22:09:34 -0400 Subject: [PATCH] Update argparse to get multiline alignment --- thirdparty/argparse/.clang-tidy | 2 +- .../workflows/tidy-analysis-stage-01.yml | 42 +++++++ .../workflows/tidy-analysis-stage-02.yml | 86 +++++++++++++ thirdparty/argparse/CMakeLists.txt | 20 ++- thirdparty/argparse/README.md | 66 +++++++--- .../argparse/include/argparse/argparse.hpp | 117 +++++++++++++----- .../argparse/samples/compound_arguments.cpp | 2 +- .../samples/custom_assignment_characters.cpp | 2 +- .../samples/custom_prefix_characters.cpp | 2 +- .../samples/gathering_remaining_arguments.cpp | 2 +- thirdparty/argparse/samples/is_used.cpp | 2 +- .../joining_repeated_optional_arguments.cpp | 2 +- .../argparse/samples/list_of_arguments.cpp | 2 +- .../argparse/samples/negative_numbers.cpp | 2 +- .../samples/optional_flag_argument.cpp | 2 +- .../argparse/samples/positional_argument.cpp | 2 +- .../samples/required_optional_argument.cpp | 2 +- thirdparty/argparse/samples/subcommands.cpp | 2 +- thirdparty/argparse/test/CMakeLists.txt | 2 + .../argparse/test/test_as_container.cpp | 52 ++++++++ .../argparse/test/test_default_args.cpp | 12 ++ .../argparse/test/test_default_value.cpp | 21 ++++ thirdparty/argparse/test/test_get.cpp | 7 ++ thirdparty/argparse/test/test_help.cpp | 45 +++++++ 24 files changed, 418 insertions(+), 78 deletions(-) create mode 100644 thirdparty/argparse/.github/workflows/tidy-analysis-stage-01.yml create mode 100644 thirdparty/argparse/.github/workflows/tidy-analysis-stage-02.yml create mode 100644 thirdparty/argparse/test/test_as_container.cpp create mode 100644 thirdparty/argparse/test/test_default_value.cpp diff --git a/thirdparty/argparse/.clang-tidy b/thirdparty/argparse/.clang-tidy index d0b28f4f79..7d0c827054 100644 --- a/thirdparty/argparse/.clang-tidy +++ b/thirdparty/argparse/.clang-tidy @@ -18,4 +18,4 @@ CheckOptions: - { key: readability-identifier-naming.StructIgnoredRegexp, value: "parse_number" } - { key: readability-identifier-naming.VariableCase, value: lower_case } -HeaderFilterRegex: '.*' +HeaderFilterRegex: 'argparse/.+\.hpp' diff --git a/thirdparty/argparse/.github/workflows/tidy-analysis-stage-01.yml b/thirdparty/argparse/.github/workflows/tidy-analysis-stage-01.yml new file mode 100644 index 0000000000..66ca0a1de3 --- /dev/null +++ b/thirdparty/argparse/.github/workflows/tidy-analysis-stage-01.yml @@ -0,0 +1,42 @@ +# Insecure workflow with limited permissions that should provide analysis +# results through an artifact. +name: Tidy analysis + +on: pull_request + +jobs: + + clang-tidy: + + runs-on: ubuntu-20.04 + + steps: + + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Install clang-tidy + run: | + sudo apt-get update + sudo apt-get install -y clang-tidy-12 + + - name: Prepare compile_commands.json + run: cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Create results directory + run: mkdir clang-tidy-result + + - name: Analyze + run: git diff -U0 HEAD^ | clang-tidy-diff-12.py -p1 -regex ".+hpp" -extra-arg=-Iinclude -extra-arg=-std=c++17 -export-fixes clang-tidy-result/fixes.yml + + - name: Save PR metadata + run: | + echo ${{ github.event.number }} > clang-tidy-result/pr-id.txt + echo ${{ github.event.pull_request.head.repo.full_name }} > clang-tidy-result/pr-head-repo.txt + echo ${{ github.event.pull_request.head.ref }} > clang-tidy-result/pr-head-ref.txt + + - uses: actions/upload-artifact@v2 + with: + name: clang-tidy-result + path: clang-tidy-result/ diff --git a/thirdparty/argparse/.github/workflows/tidy-analysis-stage-02.yml b/thirdparty/argparse/.github/workflows/tidy-analysis-stage-02.yml new file mode 100644 index 0000000000..61a60de5d0 --- /dev/null +++ b/thirdparty/argparse/.github/workflows/tidy-analysis-stage-02.yml @@ -0,0 +1,86 @@ +# Secure workflow with access to repository secrets and GitHub token +# for posting analysis results. +name: Post the Tidy analysis results + +on: + workflow_run: + workflows: [ "Tidy analysis" ] + types: [ completed ] + +jobs: + + clang-tidy-results: + + # Trigger the job only if the previous (insecure) workflow completed successfully + if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-20.04 + + steps: + + - name: Download analysis results + uses: actions/github-script@v3.1.0 + with: + script: | + let artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + let matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "clang-tidy-result" + })[0]; + let download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: "zip", + }); + let fs = require("fs"); + fs.writeFileSync("${{github.workspace}}/clang-tidy-result.zip", Buffer.from(download.data)); + + - name: Set environment variables + run: | + mkdir clang-tidy-result + unzip clang-tidy-result.zip -d clang-tidy-result + echo "pr_id=$(cat clang-tidy-result/pr-id.txt)" >> $GITHUB_ENV + echo "pr_head_repo=$(cat clang-tidy-result/pr-head-repo.txt)" >> $GITHUB_ENV + echo "pr_head_ref=$(cat clang-tidy-result/pr-head-ref.txt)" >> $GITHUB_ENV + + - uses: actions/checkout@v3 + with: + repository: ${{ env.pr_head_repo }} + ref: ${{ env.pr_head_ref }} + persist-credentials: false + + - name: Redownload analysis results + uses: actions/github-script@v3.1.0 + with: + script: | + let artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + let matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "clang-tidy-result" + })[0]; + let download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: "zip", + }); + let fs = require("fs"); + fs.writeFileSync("${{github.workspace}}/clang-tidy-result.zip", Buffer.from(download.data)); + + - name: Extract analysis results + run: | + mkdir clang-tidy-result + unzip clang-tidy-result.zip -d clang-tidy-result + + - name: Run clang-tidy-pr-comments action + uses: platisd/clang-tidy-pr-comments@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + clang_tidy_fixes: clang-tidy-result/fixes.yml + pull_request_id: ${{ env.pr_id }} diff --git a/thirdparty/argparse/CMakeLists.txt b/thirdparty/argparse/CMakeLists.txt index 19901e58b5..19d894207d 100644 --- a/thirdparty/argparse/CMakeLists.txt +++ b/thirdparty/argparse/CMakeLists.txt @@ -7,23 +7,15 @@ project(argparse LANGUAGES CXX ) -option(ARGPARSE_INSTALL ON) -option(ARGPARSE_BUILD_TESTS OFF) -option(ARGPARSE_LONG_VERSION_ARG_ONLY OFF) +option(ARGPARSE_INSTALL "Include an install target" ON) +option(ARGPARSE_BUILD_TESTS "Build tests" OFF) include(GNUInstallDirs) include(CMakePackageConfigHelpers) -string(REPLACE "/${CMAKE_LIBRARY_ARCHITECTURE}" "" CMAKE_INSTALL_LIBDIR_ARCHIND "${CMAKE_INSTALL_LIBDIR}") - add_library(argparse INTERFACE) add_library(argparse::argparse ALIAS argparse) - -if (ARGPARSE_LONG_VERSION_ARG_ONLY) - target_compile_definitions(argparse INTERFACE ARGPARSE_LONG_VERSION_ARG_ONLY=true) -endif () - target_compile_features(argparse INTERFACE cxx_std_17) target_include_directories(argparse INTERFACE $ @@ -41,7 +33,7 @@ if(ARGPARSE_INSTALL) install(TARGETS argparse EXPORT argparseConfig) install(EXPORT argparseConfig NAMESPACE argparse:: - DESTINATION ${CMAKE_INSTALL_LIBDIR_ARCHIND}/cmake/${PROJECT_NAME}) + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) install(FILES ${CMAKE_CURRENT_LIST_DIR}/include/argparse/argparse.hpp DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/argparse) @@ -64,7 +56,7 @@ if(ARGPARSE_INSTALL) NAMESPACE argparse::) install(FILES "${CMAKE_CONFIG_VERSION_FILE_NAME}" - DESTINATION "${CMAKE_INSTALL_LIBDIR_ARCHIND}/cmake/${PROJECT_NAME}") + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") set(PackagingTemplatesDir "${CMAKE_CURRENT_SOURCE_DIR}/packaging") @@ -94,6 +86,8 @@ if(ARGPARSE_INSTALL) set(PKG_CONFIG_FILE_NAME "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc") configure_file("${PackagingTemplatesDir}/pkgconfig.pc.in" "${PKG_CONFIG_FILE_NAME}" @ONLY) install(FILES "${PKG_CONFIG_FILE_NAME}" - DESTINATION "${CMAKE_INSTALL_LIBDIR_ARCHIND}/pkgconfig" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" ) endif() + +include(CPack) diff --git a/thirdparty/argparse/README.md b/thirdparty/argparse/README.md index 078489a15b..5c7957651d 100644 --- a/thirdparty/argparse/README.md +++ b/thirdparty/argparse/README.md @@ -3,7 +3,6 @@

- travis license @@ -68,6 +67,8 @@ argparse::ArgumentParser program("program_name"); **NOTE:** There is an optional second argument to the `ArgumentParser` which is the program version. Example: `argparse::ArgumentParser program("libfoo", "1.9.0");` +**NOTE:** There are optional third and fourth arguments to the `ArgumentParser` which control default arguments. Example: `argparse::ArgumentParser program("libfoo", "1.9.0", default_arguments::help, false);` See [Default Arguments](#default-arguments), below. + To add a new argument, simply call ```.add_argument(...)```. You can provide a variadic list of argument names that you want to group together, e.g., ```-v``` and ```--verbose``` ```cpp @@ -97,7 +98,7 @@ int main(int argc, char *argv[]) { catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } auto input = program.get("square"); @@ -557,7 +558,9 @@ The grammar follows `std::from_chars`, but does not exactly duplicate it. For ex ### Default Arguments -`argparse` provides predefined arguments and actions for `-h`/`--help` and `-v`/`--version`. These default actions exit the program after displaying a help or version message, respectively. These defaults arguments can be disabled during `ArgumentParser` creation so that you can handle these arguments in your own way. (Note that a program name and version must be included when choosing default arguments.) +`argparse` provides predefined arguments and actions for `-h`/`--help` and `-v`/`--version`. By default, these actions will **exit** the program after displaying a help or version message, respectively. This exit does not call destructors, skipping clean-up of taken resources. + +These default arguments can be disabled during `ArgumentParser` creation so that you can handle these arguments in your own way. (Note that a program name and version must be included when choosing default arguments.) ```cpp argparse::ArgumentParser program("test", "1.0", default_arguments::none); @@ -576,6 +579,12 @@ The above code snippet outputs a help message and continues to run. It does not The default is `default_arguments::all` for included arguments. No default arguments will be added with `default_arguments::none`. `default_arguments::help` and `default_arguments::version` will individually add `--help` and `--version`. +The default arguments can be used while disabling the default exit with these arguments. This forth argument to `ArgumentParser` (`exit_on_default_arguments`) is a bool flag with a default **true** value. The following call will retain `--help` and `--version`, but will not exit when those arguments are used. + +```cpp +argparse::ArgumentParser program("test", "1.0", default_arguments::all, false) +``` + ### Gathering Remaining Arguments `argparse` supports gathering "remaining" arguments at the end of the command, e.g., for use in a compiler: @@ -685,25 +694,30 @@ main ### Parent Parsers -Sometimes, several parsers share a common set of arguments. Rather than repeating the definitions of these arguments, a single parser with all the common arguments can be added as a parent to another ArgumentParser instance. The ```.add_parents``` method takes a list of ArgumentParser objects, collects all the positional and optional actions from them, and adds these actions to the ArgumentParser object being constructed: +A parser may use arguments that could be used by other parsers. + +These shared arguments can be added to a parser which is then used as a "parent" for parsers which also need those arguments. One or more parent parsers may be added to a parser with `.add_parents`. The positional and optional arguments in each parent is added to the child parser. ```cpp -argparse::ArgumentParser parent_parser("main"); -parent_parser.add_argument("--parent") +argparse::ArgumentParser surface_parser("surface", 1.0, argparse::default_arguments::none); +parent_parser.add_argument("--area") .default_value(0) .scan<'i', int>(); -argparse::ArgumentParser foo_parser("foo"); -foo_parser.add_argument("foo"); -foo_parser.add_parents(parent_parser); -foo_parser.parse_args({ "./main", "--parent", "2", "XXX" }); // parent = 2, foo = XXX +argparse::ArgumentParser floor_parser("floor"); +floor_parser.add_argument("tile_size").scan<'i', int>(); +floor_parser.add_parents(surface_parser); +floor_parser.parse_args({ "./main", "--area", "200", "12" }); // --area = 200, tile_size = 12 -argparse::ArgumentParser bar_parser("bar"); -bar_parser.add_argument("--bar"); -bar_parser.parse_args({ "./main", "--bar", "YYY" }); // bar = YYY +argparse::ArgumentParser ceiling_parser("ceiling"); +ceiling_parser.add_argument("--color"); +ceiling_parser.add_parents(surface_parser); +ceiling_parser.parse_args({ "./main", "--color", "gray" }); // --area = 0, --color = "gray" ``` -Note You must fully initialize the parsers before passing them via ```.add_parents```. If you change the parent parsers after the child parser, those changes will not be reflected in the child. +Changes made to parents after they are added to a parser are not reflected in any child parsers. Completely initialize parent parsers before adding them to a parser. + +Each parser will have the standard set of default arguments. Disable the default arguments in parent parsers to avoid duplicate help output. ### Subcommands @@ -766,7 +780,7 @@ int main(int argc, char *argv[]) { catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } // Use arguments @@ -827,6 +841,20 @@ When a help message is requested from a subparser, only the help for that partic Additionally, every parser has the `.is_subcommand_used("")` and `.is_subcommand_used(subparser)` member functions to check if a subcommand was used. +### Getting Argument and Subparser Instances + +```Argument``` and ```ArgumentParser``` instances added to an ```ArgumentParser``` can be retrieved with ```.at()```. The default return type is ```Argument```. + +```cpp +argparse::ArgumentParser program("test"); + +program.add_argument("--dir"); +program.at("--dir").default_value(std::string("/home/user")); + +program.add_subparser(argparse::ArgumentParser{"walk"}); +program.at("walk").add_argument("depth"); +``` + ### Parse Known Args Sometimes a program may only parse a few of the command-line arguments, passing the remaining arguments on to another script or program. In these cases, the `parse_known_args()` function can be useful. It works much like `parse_args()` except that it does not produce an error when extra arguments are present. Instead, it returns a list of remaining argument strings. @@ -879,7 +907,7 @@ int main(int argc, char *argv[]) { catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program.is_used("+f")) { @@ -927,7 +955,7 @@ int main(int argc, char *argv[]) { catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program.is_used("--foo")) { @@ -1075,7 +1103,7 @@ int main(int argc, char *argv[]) { catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program.is_used("--foo")) { @@ -1141,7 +1169,7 @@ sudo make install | :------------------- | :--------------- | :----------------- | | GCC >= 8.3.0 | libstdc++ | Ubuntu 18.04 | | Clang >= 7.0.0 | libc++ | Xcode 10.2 | -| MSVC >= 14.16 | Microsoft STL | Visual Studio 2017 | +| MSVC >= 16.8 | Microsoft STL | Visual Studio 2019 | ## Contributing Contributions are welcome, have a look at the [CONTRIBUTING.md](CONTRIBUTING.md) document for more information. diff --git a/thirdparty/argparse/include/argparse/argparse.hpp b/thirdparty/argparse/include/argparse/argparse.hpp index f4e4c6b248..a779e014ea 100644 --- a/thirdparty/argparse/include/argparse/argparse.hpp +++ b/thirdparty/argparse/include/argparse/argparse.hpp @@ -376,7 +376,8 @@ class Argument { explicit Argument(std::string_view prefix_chars, std::array &&a, std::index_sequence /*unused*/) - : m_is_optional((is_optional(a[I], prefix_chars) || ...)), + : m_accepts_optional_like_value(false), + m_is_optional((is_optional(a[I], prefix_chars) || ...)), m_is_required(false), m_is_repeatable(false), m_is_used(false), m_prefix_chars(prefix_chars) { ((void)m_names.emplace_back(a[I]), ...); @@ -408,6 +409,10 @@ public: return *this; } + Argument &default_value(const char *value) { + return default_value(std::string(value)); + } + Argument &required() { m_is_required = true; return *this; @@ -501,10 +506,10 @@ public: m_num_args_range = NArgsRange{0, 1}; break; case nargs_pattern::any: - m_num_args_range = NArgsRange{0, std::numeric_limits::max()}; + m_num_args_range = NArgsRange{0, (std::numeric_limits::max)()}; break; case nargs_pattern::at_least_one: - m_num_args_range = NArgsRange{1, std::numeric_limits::max()}; + m_num_args_range = NArgsRange{1, (std::numeric_limits::max)()}; break; } return *this; @@ -668,7 +673,36 @@ public: name_stream << " " << argument.m_metavar; } } - stream << name_stream.str() << "\t" << argument.m_help; + + // align multiline help message + auto stream_width = stream.width(); + auto name_padding = std::string(name_stream.str().size(), ' '); + auto pos = 0; + auto prev = 0; + auto first_line = true; + auto hspace = " "; // minimal space between name and help message + stream << name_stream.str(); + std::string_view help_view(argument.m_help); + while ((pos = argument.m_help.find('\n', prev)) != std::string::npos) { + auto line = help_view.substr(prev, pos - prev + 1); + if (first_line) { + stream << hspace << line; + first_line = false; + } else { + stream.width(stream_width); + stream << name_padding << hspace << line; + } + prev += pos - prev + 1; + } + if (first_line) { + stream << hspace << argument.m_help; + } else { + auto leftover = help_view.substr(prev, argument.m_help.size() - prev); + if (!leftover.empty()) { + stream.width(stream_width); + stream << name_padding << hspace << leftover; + } + } // print nargs spec if (!argument.m_help.empty()) { @@ -698,10 +732,13 @@ public: if constexpr (!details::IsContainer) { return get() == rhs; } else { + using ValueType = typename T::value_type; auto lhs = get(); return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs), - [](const auto &a, const auto &b) { return a == b; }); + [](const auto &a, const auto &b) { + return std::any_cast(a) == b; + }); } } @@ -725,7 +762,7 @@ private: bool is_exact() const { return m_min == m_max; } bool is_right_bounded() const { - return m_max < std::numeric_limits::max(); + return m_max < (std::numeric_limits::max)(); } std::size_t get_min() const { return m_min; } @@ -740,7 +777,7 @@ private: stream << "[nargs: " << range.m_min << "] "; } } else { - if (range.m_max == std::numeric_limits::max()) { + if (range.m_max == (std::numeric_limits::max)()) { stream << "[nargs: " << range.m_min << " or more] "; } else { stream << "[nargs=" << range.m_min << ".." << range.m_max << "] "; @@ -968,18 +1005,16 @@ private: * Get argument value given a type * @throws std::logic_error in case of incompatible types */ - template - auto get() const - -> std::conditional_t, T, const T &> { + template T get() const { if (!m_values.empty()) { if constexpr (details::IsContainer) { return any_cast_container(m_values); } else { - return *std::any_cast(&m_values.front()); + return std::any_cast(m_values.front()); } } if (m_default_value.has_value()) { - return *std::any_cast(&m_default_value); + return std::any_cast(m_default_value); } if constexpr (details::IsContainer) { if (!m_accepts_optional_like_value) { @@ -1015,7 +1050,7 @@ private: T result; std::transform( std::begin(operand), std::end(operand), std::back_inserter(result), - [](const auto &value) { return *std::any_cast(&value); }); + [](const auto &value) { return std::any_cast(value); }); return result; } @@ -1033,11 +1068,12 @@ private: [](const std::string &value) { return value; }}; std::vector m_values; NArgsRange m_num_args_range{1, 1}; - bool m_accepts_optional_like_value = false; - bool m_is_optional : true; - bool m_is_required : true; - bool m_is_repeatable : true; - bool m_is_used : true; // True if the optional argument is used by user + // Bit field of bool values. Set default value in ctor. + bool m_accepts_optional_like_value : 1; + bool m_is_optional : 1; + bool m_is_required : 1; + bool m_is_repeatable : 1; + bool m_is_used : 1; std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars }; @@ -1045,14 +1081,18 @@ class ArgumentParser { public: explicit ArgumentParser(std::string program_name = {}, std::string version = "1.0", - default_arguments add_args = default_arguments::all) + default_arguments add_args = default_arguments::all, + bool exit_on_default_arguments = true) : m_program_name(std::move(program_name)), m_version(std::move(version)), + m_exit_on_default_arguments(exit_on_default_arguments), m_parser_path(m_program_name) { if ((add_args & default_arguments::help) == default_arguments::help) { add_argument("-h", "--help") .action([&](const auto & /*unused*/) { std::cout << help().str(); - std::exit(0); + if (m_exit_on_default_arguments) { + std::exit(0); + } }) .default_value(false) .help("shows help message and exits") @@ -1063,7 +1103,9 @@ public: add_argument("-v", "--version") .action([&](const auto & /*unused*/) { std::cout << m_version << std::endl; - std::exit(0); + if (m_exit_on_default_arguments) { + std::exit(0); + } }) .default_value(false) .help("prints version information and exits") @@ -1167,6 +1209,22 @@ public: return *this; } + /* Getter for arguments and subparsers. + * @throws std::logic_error in case of an invalid argument or subparser name + */ + template + T& at(std::string_view name) { + if constexpr (std::is_same_v) { + return (*this)[name]; + } else { + auto subparser_it = m_subparser_map.find(name); + if (subparser_it != m_subparser_map.end()) { + return subparser_it->second->get(); + } + throw std::logic_error("No such subparser: " + std::string(name)); + } + } + ArgumentParser &set_prefix_chars(std::string prefix_chars) { m_prefix_chars = std::move(prefix_chars); return *this; @@ -1229,9 +1287,7 @@ public: * @throws std::logic_error if the option has no value * @throws std::bad_any_cast if the option is not of type T */ - template - auto get(std::string_view arg_name) const - -> std::conditional_t, T, const T &> { + template T get(std::string_view arg_name) const { if (!m_is_parsed) { throw std::logic_error("Nothing parsed, no arguments are available."); } @@ -1365,13 +1421,7 @@ public: // Add any options inline here for (const auto &argument : this->m_optional_arguments) { - if (argument.m_names.front() == "-v") { - continue; - } else if (argument.m_names.front() == "-h") { - stream << " [-h]"; - } else { - stream << " " << argument.get_inline_usage(); - } + stream << " " << argument.get_inline_usage(); } // Put positional arguments after the optionals for (const auto &argument : this->m_positional_arguments) { @@ -1639,10 +1689,10 @@ private: } std::size_t max_size = 0; for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { - max_size = std::max(max_size, argument->get_arguments_length()); + max_size = std::max(max_size, argument->get_arguments_length()); } for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) { - max_size = std::max(max_size, command.size()); + max_size = std::max(max_size, command.size()); } return max_size; } @@ -1661,6 +1711,7 @@ private: std::string m_version; std::string m_description; std::string m_epilog; + bool m_exit_on_default_arguments = true; std::string m_prefix_chars{"-"}; std::string m_assign_chars{"="}; bool m_is_parsed = false; diff --git a/thirdparty/argparse/samples/compound_arguments.cpp b/thirdparty/argparse/samples/compound_arguments.cpp index 66d5c920e1..4cfc037fcd 100644 --- a/thirdparty/argparse/samples/compound_arguments.cpp +++ b/thirdparty/argparse/samples/compound_arguments.cpp @@ -19,7 +19,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } auto a = program.get("-a"); // true diff --git a/thirdparty/argparse/samples/custom_assignment_characters.cpp b/thirdparty/argparse/samples/custom_assignment_characters.cpp index d5deff126b..7e35ae0b19 100644 --- a/thirdparty/argparse/samples/custom_assignment_characters.cpp +++ b/thirdparty/argparse/samples/custom_assignment_characters.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program.is_used("--foo")) { diff --git a/thirdparty/argparse/samples/custom_prefix_characters.cpp b/thirdparty/argparse/samples/custom_prefix_characters.cpp index 01e248a650..9f8917a366 100644 --- a/thirdparty/argparse/samples/custom_prefix_characters.cpp +++ b/thirdparty/argparse/samples/custom_prefix_characters.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program.is_used("+f")) { diff --git a/thirdparty/argparse/samples/gathering_remaining_arguments.cpp b/thirdparty/argparse/samples/gathering_remaining_arguments.cpp index 4f404544f1..be13f10f9b 100644 --- a/thirdparty/argparse/samples/gathering_remaining_arguments.cpp +++ b/thirdparty/argparse/samples/gathering_remaining_arguments.cpp @@ -12,7 +12,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } try { diff --git a/thirdparty/argparse/samples/is_used.cpp b/thirdparty/argparse/samples/is_used.cpp index 27e0373b23..ffc05f880a 100644 --- a/thirdparty/argparse/samples/is_used.cpp +++ b/thirdparty/argparse/samples/is_used.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } auto color = program.get("--color"); // "orange" diff --git a/thirdparty/argparse/samples/joining_repeated_optional_arguments.cpp b/thirdparty/argparse/samples/joining_repeated_optional_arguments.cpp index eebbdc6170..0f6ab8159f 100644 --- a/thirdparty/argparse/samples/joining_repeated_optional_arguments.cpp +++ b/thirdparty/argparse/samples/joining_repeated_optional_arguments.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } auto colors = program.get>( diff --git a/thirdparty/argparse/samples/list_of_arguments.cpp b/thirdparty/argparse/samples/list_of_arguments.cpp index e533e477ee..996e7480e7 100644 --- a/thirdparty/argparse/samples/list_of_arguments.cpp +++ b/thirdparty/argparse/samples/list_of_arguments.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } auto files = program.get>( diff --git a/thirdparty/argparse/samples/negative_numbers.cpp b/thirdparty/argparse/samples/negative_numbers.cpp index ac4284fb4d..319245fe4b 100644 --- a/thirdparty/argparse/samples/negative_numbers.cpp +++ b/thirdparty/argparse/samples/negative_numbers.cpp @@ -17,7 +17,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program.is_used("integer")) { diff --git a/thirdparty/argparse/samples/optional_flag_argument.cpp b/thirdparty/argparse/samples/optional_flag_argument.cpp index 2f859540fd..f935ecf0be 100644 --- a/thirdparty/argparse/samples/optional_flag_argument.cpp +++ b/thirdparty/argparse/samples/optional_flag_argument.cpp @@ -15,7 +15,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } if (program["--verbose"] == true) { diff --git a/thirdparty/argparse/samples/positional_argument.cpp b/thirdparty/argparse/samples/positional_argument.cpp index 03ffb0a68e..4343863ce3 100644 --- a/thirdparty/argparse/samples/positional_argument.cpp +++ b/thirdparty/argparse/samples/positional_argument.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } int input = program.get("square"); diff --git a/thirdparty/argparse/samples/required_optional_argument.cpp b/thirdparty/argparse/samples/required_optional_argument.cpp index efea523154..0271f500b3 100644 --- a/thirdparty/argparse/samples/required_optional_argument.cpp +++ b/thirdparty/argparse/samples/required_optional_argument.cpp @@ -14,7 +14,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } std::cout << "Output written to " << program.get("-o") << "\n"; diff --git a/thirdparty/argparse/samples/subcommands.cpp b/thirdparty/argparse/samples/subcommands.cpp index 74d610e14c..ba1059dfc7 100644 --- a/thirdparty/argparse/samples/subcommands.cpp +++ b/thirdparty/argparse/samples/subcommands.cpp @@ -60,7 +60,7 @@ int main(int argc, char *argv[]) { } catch (const std::runtime_error &err) { std::cerr << err.what() << std::endl; std::cerr << program; - std::exit(1); + return 1; } // Use arguments diff --git a/thirdparty/argparse/test/CMakeLists.txt b/thirdparty/argparse/test/CMakeLists.txt index 47b9ba5c7b..a903f09f90 100644 --- a/thirdparty/argparse/test/CMakeLists.txt +++ b/thirdparty/argparse/test/CMakeLists.txt @@ -27,11 +27,13 @@ file(GLOB ARGPARSE_TEST_SOURCES main.cpp test_actions.cpp test_append.cpp + test_as_container.cpp test_bool_operator.cpp test_compound_arguments.cpp test_container_arguments.cpp test_const_correct.cpp test_default_args.cpp + test_default_value.cpp test_get.cpp test_help.cpp test_invalid_arguments.cpp diff --git a/thirdparty/argparse/test/test_as_container.cpp b/thirdparty/argparse/test/test_as_container.cpp new file mode 100644 index 0000000000..2f795fbf6c --- /dev/null +++ b/thirdparty/argparse/test/test_as_container.cpp @@ -0,0 +1,52 @@ +#include +#include + +using doctest::test_suite; + +TEST_CASE("Get argument with .at()" * test_suite("as_container")) { + argparse::ArgumentParser program("test"); + auto &dir_arg = program.add_argument("--dir"); + program.at("--dir").default_value(std::string("/home/user")); + + SUBCASE("and default value") { + program.parse_args({"test"}); + REQUIRE(&(program.at("--dir")) == &dir_arg); + REQUIRE(program["--dir"] == std::string("/home/user")); + REQUIRE(program.at("--dir") == std::string("/home/user")); + } + + SUBCASE("and user-supplied value") { + program.parse_args({"test", "--dir", "/usr/local/database"}); + REQUIRE(&(program.at("--dir")) == &dir_arg); + REQUIRE(program["--dir"] == std::string("/usr/local/database")); + REQUIRE(program.at("--dir") == std::string("/usr/local/database")); + } + + SUBCASE("with unknown argument") { + program.parse_args({"test"}); + REQUIRE_THROWS_WITH_AS(program.at("--folder"), + "No such argument: --folder", std::logic_error); + } +} + +TEST_CASE("Get subparser with .at()" * test_suite("as_container")) { + argparse::ArgumentParser program("test"); + + argparse::ArgumentParser walk_cmd("walk"); + auto &speed = walk_cmd.add_argument("speed"); + + program.add_subparser(walk_cmd); + + SUBCASE("and its argument") { + program.parse_args({"test", "walk", "4km/h"}); + REQUIRE(&(program.at("walk")) == &walk_cmd); + REQUIRE(&(program.at("walk").at("speed")) == &speed); + REQUIRE(program.at("walk").is_used("speed")); + } + + SUBCASE("with unknown command") { + program.parse_args({"test"}); + REQUIRE_THROWS_WITH_AS(program.at("fly"), + "No such subparser: fly", std::logic_error); + } +} diff --git a/thirdparty/argparse/test/test_default_args.cpp b/thirdparty/argparse/test/test_default_args.cpp index 7b5e581f29..d0cdd25330 100644 --- a/thirdparty/argparse/test/test_default_args.cpp +++ b/thirdparty/argparse/test/test_default_args.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include using doctest::test_suite; @@ -17,3 +19,13 @@ TEST_CASE("Do not include default arguments" * test_suite("default_args")) { REQUIRE_THROWS_AS(parser.get("--help"), std::logic_error); REQUIRE_THROWS_AS(parser.get("--version"), std::logic_error); } + +TEST_CASE("Do not exit on default arguments" * test_suite("default_args")) { + argparse::ArgumentParser parser("test", "1.0", + argparse::default_arguments::all, false); + std::stringstream buf; + std::streambuf* saved_cout_buf = std::cout.rdbuf(buf.rdbuf()); + parser.parse_args({"test", "--help"}); + std::cout.rdbuf(saved_cout_buf); + REQUIRE(parser.is_used("--help")); +} diff --git a/thirdparty/argparse/test/test_default_value.cpp b/thirdparty/argparse/test/test_default_value.cpp new file mode 100644 index 0000000000..15271f96b4 --- /dev/null +++ b/thirdparty/argparse/test/test_default_value.cpp @@ -0,0 +1,21 @@ +#include +#include +#include + +using doctest::test_suite; + +TEST_CASE("Use a 'string' default value" * test_suite("default_value")) { + argparse::ArgumentParser program("test"); + + SUBCASE("Use a const char[] default value") { + program.add_argument("--arg").default_value("array of char"); + REQUIRE_NOTHROW(program.parse_args({"test"})); + REQUIRE(program.get("--arg") == std::string("array of char")); + } + + SUBCASE("Use a std::string default value") { + program.add_argument("--arg").default_value(std::string("string object")); + REQUIRE_NOTHROW(program.parse_args({"test"})); + REQUIRE(program.get("--arg") == std::string("string object")); + } +} diff --git a/thirdparty/argparse/test/test_get.cpp b/thirdparty/argparse/test/test_get.cpp index 9cc046c8d1..ad719b0505 100644 --- a/thirdparty/argparse/test/test_get.cpp +++ b/thirdparty/argparse/test/test_get.cpp @@ -33,3 +33,10 @@ TEST_CASE("Implicit argument" * test_suite("ArgumentParser::get")) { REQUIRE_THROWS_WITH_AS(program.get("--stuff"), "No value provided for '--stuff'.", std::logic_error); } + +TEST_CASE("Mismatched type for argument" * test_suite("ArgumentParser::get")) { + argparse::ArgumentParser program("test"); + program.add_argument("-s", "--stuff"); // as default type, a std::string + REQUIRE_NOTHROW(program.parse_args({"test", "-s", "321"})); + REQUIRE_THROWS_AS(program.get("--stuff"), std::bad_any_cast); +} diff --git a/thirdparty/argparse/test/test_help.cpp b/thirdparty/argparse/test/test_help.cpp index a81af42284..293daef187 100644 --- a/thirdparty/argparse/test/test_help.cpp +++ b/thirdparty/argparse/test/test_help.cpp @@ -73,3 +73,48 @@ TEST_CASE("Users can replace default -h/--help" * test_suite("help")) { program.parse_args({"test", "--help"}); REQUIRE_FALSE(buffer.str().empty()); } + +TEST_CASE("Multiline help message alignment") { + // '#' is used at the beginning of each help message line to simplify testing. + // It is important to ensure that this character doesn't appear elsewhere in the test case. + // Default arguments (e.g., -h/--help, -v/--version) are not included in this test. + argparse::ArgumentParser program("program"); + program.add_argument("INPUT1") + .help( + "#This is the first line of help message.\n" + "#And this is the second line of help message." + ); + program.add_argument("program_input2") + .help("#There is only one line."); + program.add_argument("-p", "--prog_input3") + .help( +R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit. +#Sed ut perspiciatis unde omnis iste natus error sit voluptatem +#accusantium doloremque laudantium, totam rem aperiam...)" + ); + program.add_argument("--verbose").default_value(false).implicit_value(true); + + std::ostringstream stream; + stream << program; + std::istringstream iss(stream.str()); + + int help_message_start = -1; + std::string line; + while (std::getline(iss, line)) { + // Find the position of '#', which indicates the start of the help message line + auto pos = line.find('#'); + + if (pos == std::string::npos) { + continue; + } + + if (help_message_start == -1) { + help_message_start = pos; + } else { + REQUIRE(pos == help_message_start); + } + } + + // Make sure we have at least one help message + REQUIRE(help_message_start != -1); +}