diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index bf8de357..64d1b76c 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -906,7 +906,6 @@ class Argument { } std::size_t get_arguments_length() const { - std::size_t names_size = std::accumulate( std::begin(m_names), std::end(m_names), std::size_t(0), [](const auto &sum, const auto &s) { return sum + s.size(); }); @@ -1362,15 +1361,14 @@ class ArgumentParser { explicit ArgumentParser(std::string program_name = {}, std::string version = "1.0", default_arguments add_args = default_arguments::all, - bool exit_on_default_arguments = true, - std::ostream &os = std::cout) + 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*/) { - os << help().str(); + .action([this](const auto & /*unused*/) { + std::cout << help().str(); if (m_exit_on_default_arguments) { std::exit(0); } @@ -1379,11 +1377,12 @@ class ArgumentParser { .help("shows help message and exits") .implicit_value(true) .nargs(0); + m_default_help_argument_added = true; } if ((add_args & default_arguments::version) == default_arguments::version) { add_argument("-v", "--version") - .action([&](const auto & /*unused*/) { - os << m_version << std::endl; + .action([this](const auto & /*unused*/) { + std::cout << m_version << std::endl; if (m_exit_on_default_arguments) { std::exit(0); } @@ -1392,6 +1391,7 @@ class ArgumentParser { .help("prints version information and exits") .implicit_value(true) .nargs(0); + m_default_version_argument_added = true; } } @@ -1400,7 +1400,11 @@ class ArgumentParser { ArgumentParser(const ArgumentParser &other) : m_program_name(other.m_program_name), m_version(other.m_version), + m_default_help_argument_added(other.m_default_help_argument_added), + m_default_version_argument_added( + other.m_default_version_argument_added), m_description(other.m_description), m_epilog(other.m_epilog), + m_exit_on_default_arguments(other.m_exit_on_default_arguments), m_prefix_chars(other.m_prefix_chars), m_assign_chars(other.m_assign_chars), m_is_parsed(other.m_is_parsed), m_positional_arguments(other.m_positional_arguments), @@ -1414,6 +1418,32 @@ class ArgumentParser { it != std::end(m_optional_arguments); ++it) { index_argument(it); } + + // Redefine help action + if (m_default_help_argument_added) { + auto help_argument = m_argument_map["--help"]; + help_argument->action([this](const auto & /*unused*/) { + std::cout << help().str(); + if (m_exit_on_default_arguments) { + std::exit(0); + } + }); + } + + if (m_default_version_argument_added) { + add_argument("-v", "--version") + .action([this](const auto & /*unused*/) { + std::cout << m_version << std::endl; + if (m_exit_on_default_arguments) { + std::exit(0); + } + }) + .default_value(false) + .help("prints version information and exits") + .implicit_value(true) + .nargs(0); + } + for (auto it = std::begin(m_subparsers); it != std::end(m_subparsers); ++it) { m_subparser_map.insert_or_assign(it->get().m_program_name, it); @@ -1941,7 +1971,7 @@ class ArgumentParser { "Failed to parse '" + current_argument + "', did you mean '" + std::string{details::get_most_similar_string( m_subparser_map, current_argument)} + - "'"); + "'?"); } // Ask the user if they meant to use a specific optional argument @@ -1951,8 +1981,8 @@ class ArgumentParser { // not a flag, requires a value if (!opt.m_is_used) { throw std::runtime_error( - "Zero positional arguments expected, did you mean " + - opt.get_usage_full()); + "Zero positional arguments expected, did you mean '" + + opt.get_usage_full() + "'?"); } } } @@ -2106,6 +2136,8 @@ class ArgumentParser { std::string m_program_name; std::string m_version; + bool m_default_help_argument_added{false}; + bool m_default_version_argument_added{false}; std::string m_description; std::string m_epilog; bool m_exit_on_default_arguments = true; diff --git a/include/argparse/main.cpp b/include/argparse/main.cpp deleted file mode 100644 index dda34538..00000000 --- a/include/argparse/main.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "argparse.hpp" - -int main(int argc, char* argv[]) { - argparse::ArgumentParser program; - program.add_argument("-a", "--number-of-apples"); - program.add_argument("-b", "--bro"); - program.parse_args(argc, argv); -} \ No newline at end of file diff --git a/include/argparse/test b/include/argparse/test deleted file mode 100755 index 0844ee17..00000000 Binary files a/include/argparse/test and /dev/null differ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8e942be2..4b4c5a2b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_compound_arguments.cpp test_container_arguments.cpp test_const_correct.cpp + test_copy_constructor.cpp test_default_args.cpp test_default_value.cpp test_error_reporting.cpp @@ -50,7 +51,6 @@ file(GLOB ARGPARSE_TEST_SOURCES test_repr.cpp test_required_arguments.cpp test_scan.cpp - test_stringstream.cpp test_value_semantics.cpp test_version.cpp test_subparsers.cpp diff --git a/test/test_copy_constructor.cpp b/test/test_copy_constructor.cpp new file mode 100644 index 00000000..9e9baf15 --- /dev/null +++ b/test/test_copy_constructor.cpp @@ -0,0 +1,157 @@ +#ifdef WITH_MODULE +import argparse; +#else +#include +#endif +#include +#include + +using doctest::test_suite; + +TEST_CASE("Parse positional arguments using a copy of an ArgumentParser" * + test_suite("copy_constructor")) { + + auto maker = []() { + argparse::ArgumentParser program("test"); + program.add_argument("first"); + program.add_argument("second").nargs(2); + + return program; + }; + + auto program = maker(); + + REQUIRE_NOTHROW(program.parse_args( + {"test", "rocket.mesh", "thrust_profile.csv", "config.json"})); + + auto first = program.get("first"); + REQUIRE(first == "rocket.mesh"); + auto second = program.get>("second"); + REQUIRE(second.size() == 2); + REQUIRE(second[0] == "thrust_profile.csv"); + REQUIRE(second[1] == "config.json"); +} + +TEST_CASE("Parse optional arguments using a copy of an ArgumentParser" * + test_suite("copy_constructor")) { + + auto maker = []() { + argparse::ArgumentParser program("test"); + program.add_argument("--first"); + program.add_argument("--second").nargs(2); + + return program; + }; + + auto program = maker(); + + REQUIRE_NOTHROW( + program.parse_args({"test", "--first", "rocket.mesh", "--second", + "thrust_profile.csv", "config.json"})); + + auto first = program.get("--first"); + REQUIRE(first == "rocket.mesh"); + auto second = program.get>("--second"); + REQUIRE(second.size() == 2); + REQUIRE(second[0] == "thrust_profile.csv"); + REQUIRE(second[1] == "config.json"); +} + +TEST_CASE("Segmentation fault on help (Issue #260)" * + test_suite("copy_constructor") * doctest::skip()) { + + struct SubparserContainer { + argparse::ArgumentParser parser; + }; + + auto get_container = []() { + SubparserContainer *container = nullptr; + if (container == nullptr) { + argparse::ArgumentParser parser("subcommand", "1.0", + argparse::default_arguments::all, false); + parser.add_description("Example"); + container = new SubparserContainer{parser}; + } + return container; + }; + + argparse::ArgumentParser program("program"); + auto *container = get_container(); + program.add_subparser(container->parser); + + std::ostringstream oss; + std::streambuf *p_cout_streambuf = std::cout.rdbuf(); + std::cout.rdbuf(oss.rdbuf()); + + program.parse_args({"program", "subcommand", "-h"}); + + std::cout.rdbuf(p_cout_streambuf); // restore + + auto cmdline_output = oss.str(); + REQUIRE(cmdline_output.size() > 0); + REQUIRE(cmdline_output.find("shows help message and exits") != + std::string::npos); +} + +TEST_CASE("Segmentation fault on custom help (Issue #260)" * + test_suite("copy_constructor") * doctest::skip()) { + + struct SubparserContainer { + argparse::ArgumentParser parser; + }; + + auto get_container = []() { + SubparserContainer *container = nullptr; + if (container == nullptr) { + argparse::ArgumentParser parser("subcommand", "1.0", + argparse::default_arguments::none, false); + parser.add_description("Example"); + std::string temporary{"temp+string"}; + parser.add_argument("-h", "--help") + .flag() + .nargs(0) + .action( + [&](const auto &) -> void { std::cout << temporary << "\n"; }); + + container = new SubparserContainer{parser}; + } + return container; + }; + + argparse::ArgumentParser program("program"); + auto *container = get_container(); + program.add_subparser(container->parser); + + std::ostringstream oss; + std::streambuf *p_cout_streambuf = std::cout.rdbuf(); + std::cout.rdbuf(oss.rdbuf()); + + program.parse_args({"program", "subcommand", "-h"}); + + std::cout.rdbuf(p_cout_streambuf); // restore + + auto cmdline_output = oss.str(); + REQUIRE(cmdline_output.size() > 0); + REQUIRE(cmdline_output.find("temp+string") != std::string::npos); +} + +TEST_CASE("Assign a new subparser with assignment operator (Issue #260)") { + argparse::ArgumentParser baseParser{"program", "0.0.0.0"}; + argparse::ArgumentParser testCommand{"file-unsafe"}; + + // This is what causes references to be invalidated. + testCommand = argparse::ArgumentParser{"file-safe"}; + + testCommand.add_description("File generator command description."); + testCommand.add_argument("-p", "--path") + .default_value("some/path/on/system") + .required() + .help("Specifies the path to the target output file."); + baseParser.add_subparser(testCommand); + REQUIRE_NOTHROW(baseParser.parse_args( + {"program", "file-safe", "-p", "\"/home/foo/bar\""})); + REQUIRE(testCommand.get("-p") == + std::string{"\"/home/foo/bar\""}); + REQUIRE(baseParser.at("file-safe") + .get("-p") == std::string{"\"/home/foo/bar\""}); +} \ No newline at end of file diff --git a/test/test_error_reporting.cpp b/test/test_error_reporting.cpp index 0c911321..2b9b184b 100644 --- a/test/test_error_reporting.cpp +++ b/test/test_error_reporting.cpp @@ -23,14 +23,14 @@ TEST_CASE("Missing optional argument name" * test_suite("error_reporting")) { SUBCASE("Bad case") { REQUIRE_THROWS_WITH_AS( parser.parse_args({"test", "-a", "1", "2"}), - "Zero positional arguments expected, did you mean -b VAR", + "Zero positional arguments expected, did you mean '-b VAR'?", std::runtime_error); } SUBCASE("Bad case 2") { REQUIRE_THROWS_WITH_AS( parser.parse_args({"test", "1", "2"}), - "Zero positional arguments expected, did you mean -a VAR", + "Zero positional arguments expected, did you mean '-a VAR'?", std::runtime_error); } } @@ -50,14 +50,14 @@ TEST_CASE("Missing optional argument name (some flag arguments)" * SUBCASE("Bad case") { REQUIRE_THROWS_WITH_AS( parser.parse_args({"test", "-a", "-b", "2"}), - "Zero positional arguments expected, did you mean -c VAR", + "Zero positional arguments expected, did you mean '-c VAR'?", std::runtime_error); } SUBCASE("Bad case 2") { REQUIRE_THROWS_WITH_AS( parser.parse_args({"test", "-abc", "1", "2"}), - "Zero positional arguments expected, did you mean -d VAR", + "Zero positional arguments expected, did you mean '-d VAR'?", std::runtime_error); } } @@ -71,7 +71,7 @@ TEST_CASE("Missing optional argument name (multiple names)" * SUBCASE("Bad case 2") { REQUIRE_THROWS_WITH_AS(parser.parse_args({"test", "1", "2"}), "Zero positional arguments expected, did you mean " - "-a/--number-of-apples VAR", + "'-a/--number-of-apples VAR'?", std::runtime_error); } } @@ -106,19 +106,19 @@ TEST_CASE("Detect unknown subcommand" * test_suite("error_reporting")) { SUBCASE("Typo for 'notes'") { REQUIRE_THROWS_WITH_AS(program.parse_args({"git", "tote"}), - "Failed to parse 'tote', did you mean 'notes'", + "Failed to parse 'tote', did you mean 'notes'?", std::runtime_error); } SUBCASE("Typo for 'add'") { REQUIRE_THROWS_WITH_AS(program.parse_args({"git", "bad"}), - "Failed to parse 'bad', did you mean 'add'", + "Failed to parse 'bad', did you mean 'add'?", std::runtime_error); } SUBCASE("Typo for 'log'") { REQUIRE_THROWS_WITH_AS(program.parse_args({"git", "logic"}), - "Failed to parse 'logic', did you mean 'log'", + "Failed to parse 'logic', did you mean 'log'?", std::runtime_error); } } \ No newline at end of file diff --git a/test/test_stringstream.cpp b/test/test_stringstream.cpp deleted file mode 100644 index 105bb409..00000000 --- a/test/test_stringstream.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifdef WITH_MODULE -import argparse; -#else -#include -#endif -#include - -#include -#include -#include - -using doctest::test_suite; - -TEST_CASE("Get Version String" * test_suite("stringstream")) { - std::stringstream os; - argparse::ArgumentParser program("test", "1.0", - argparse::default_arguments::all, false, os); - program.parse_args({"test", "--version"}); - REQUIRE(os.str() == "1.0\n"); -} \ No newline at end of file