diff --git a/README.md b/README.md index ed44ab3..b6c1263 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ int main(int argc, char* argv[]) .description("Some really useful cli program.") .addHelp(); + std::string inputFilename; + parser.option("INPUT_FILENAME") + .required() + .description("Input filename.") + .store(inputFilename); + parser.option("-v", "--version") .flag() .overruling() @@ -46,6 +52,7 @@ int main(int argc, char* argv[]) if (parser.parse()) { + std::cout << "Input filename: " << inputFilename << std::endl; std::cout << "Config file: " << config << std::endl; std::cout << "Silent mode set: " << silent << std::endl; std::cout << "Flag set: " << flag.value() << std::endl; @@ -58,10 +65,12 @@ int main(int argc, char* argv[]) Sample Application 1.0.0 Some really useful cli program. -./a.out [-h] [-v] -c [-s] +./a.out [-h] INPUT_FILENAME [-v] -c [-s] [-f] [--loglevel debug|info|trace] -h --help Print this help message. +INPUT_FILENAME + Input filename. -v --version -c --cfg Sets the config file. @@ -74,7 +83,8 @@ Some really useful cli program. ``` ``` -> ./basic -c config.json -f +> ./basic test.txt -c config.json -f +Input filename: test.txt Config file: config.json Silent mode set: 0 Flag set: 1 diff --git a/examples/basic.cpp b/examples/basic.cpp index bba9e95..b396306 100644 --- a/examples/basic.cpp +++ b/examples/basic.cpp @@ -9,6 +9,12 @@ int main(int argc, char* argv[]) .description("Some really useful cli program.") .addHelp(); + std::string inputFilename; + parser.option("INPUT_FILENAME") + .required() + .description("Input filename.") + .store(inputFilename); + parser.option("-v", "--version") .flag() .overruling() @@ -41,6 +47,7 @@ int main(int argc, char* argv[]) if (parser.parse()) { + std::cout << "Input filename: " << inputFilename << std::endl; std::cout << "Config file: " << config << std::endl; std::cout << "Silent mode set: " << silent << std::endl; std::cout << "Flag set: " << flag.value() << std::endl; diff --git a/include/clapp.hpp b/include/clapp.hpp index 7cf6fb6..12387fd 100644 --- a/include/clapp.hpp +++ b/include/clapp.hpp @@ -1,7 +1,7 @@ /* ___ _ _ ___ ___ / __| | /_\ | _ \ _ \ Command Line Argument Parser++ - | (__| |__ / _ \| _/ _/ Version 1.3.1 + | (__| |__ / _ \| _/ _/ Version 1.4.0 \___|____/_/ \_\_| |_| https://github.com/sisakat/clapp Licensed under the MIT License . @@ -42,8 +42,8 @@ SOFTWARE. #include #define CLAPP_VERSION_MAJOR 1 -#define CLAPP_VERSION_MINOR 3 -#define CLAPP_VERSION_PATCH 1 +#define CLAPP_VERSION_MINOR 4 +#define CLAPP_VERSION_PATCH 0 namespace clapp { @@ -113,12 +113,15 @@ class ArgumentParser long_option{std::move(_long_option)} { } + explicit Option(const std::string& long_option) : Option("", long_option) { } + virtual void setValue(const std::string& value) = 0; - virtual bool isAllowedValue(const std::string& value) = 0; + [[nodiscard]] virtual bool isAllowedValue(const std::string& value) = 0; + [[nodiscard]] virtual bool isPositionalOption() const = 0; virtual std::set choices() = 0; virtual void invokeCallback() = 0; @@ -346,6 +349,12 @@ class ArgumentParser return false; } + [[nodiscard]] bool isPositionalOption() const override + { + return !long_option.empty() && short_option.empty() && + long_option.at(0) != '-'; + } + std::set choices() override { std::ostringstream oss; @@ -517,17 +526,31 @@ class ArgumentParser { ss << "["; } - if (!option->short_option.empty()) - { - ss << option->short_option; - } - else if (!option->long_option.empty()) + if (option->isPositionalOption()) { - ss << option->long_option; + if (!option->argument_name.empty()) + { + ss << "<" << option->argument_name << ">"; + } + else + { + ss << option->long_option; + } } - if (!option->argument_name.empty()) + else { - ss << " <" << option->argument_name << ">"; + if (!option->short_option.empty()) + { + ss << option->short_option; + } + else if (!option->long_option.empty()) + { + ss << option->long_option; + } + if (!option->argument_name.empty()) + { + ss << " <" << option->argument_name << ">"; + } } auto choices = option->choices(); if (!choices.empty()) @@ -554,6 +577,11 @@ class ArgumentParser for (const auto& option : m_options) { + if (option->isPositionalOption() && option->description.empty()) + { + continue; + } + if (!option->short_option.empty()) { ss << std::setw(2) << std::left << option->short_option; @@ -723,6 +751,7 @@ class ArgumentParser if (m_options_map.find(optionStr) != m_options_map.end()) { + // we have a proper option auto idx = m_options_map.at(optionStr); auto& option = m_options.at(idx); @@ -751,6 +780,26 @@ class ArgumentParser m_option_order.push_back(idx); } + else if (!optionStr.empty() && optionStr.at(0) == '-') + { + std::ostringstream oss; + oss << "Unknown option '" << optionStr << "'."; + throw ArgumentParserException(oss.str()); + } + else + { + // we have no proper option - possibly a positional option + for (auto& option : m_options) + { + if (option->isPositionalOption() && !option->set) + { + option->setValue(optionStr); + m_option_order.push_back( + m_options_map.at(option->long_option)); + break; + } + } + } ++m_curr_arg; } diff --git a/test/test_main.cpp b/test/test_main.cpp index 460497b..3196f9b 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -10,6 +10,7 @@ TEST_CASE("test_int_store") int out = 0; parser.option("-a").store(out); + parser.option("-b"); parser.parse(); REQUIRE(out == 123); @@ -22,6 +23,7 @@ TEST_CASE("test_double_store") double out = 0; parser.option("-a").store(out); + parser.option("-b"); parser.parse(); REQUIRE(out == 3.1415); @@ -34,6 +36,7 @@ TEST_CASE("test_string_store") std::string out; parser.option("-a").store(out); + parser.option("-b"); parser.parse(); REQUIRE(out == "something"); @@ -45,6 +48,7 @@ TEST_CASE("test_value") clapp::ArgumentParser parser(arguments); auto& option = parser.option("-a"); + parser.option("-b"); parser.parse(); REQUIRE(option.value() == "something"); @@ -55,6 +59,8 @@ TEST_CASE("test_default_value") std::vector arguments{"", "-x", "something", "-b", "hello"}; clapp::ArgumentParser parser(arguments); + parser.option("-x"); + parser.option("-b"); auto& option = parser.option("-a").defaultValue("abc"); parser.parse(); @@ -120,6 +126,7 @@ TEST_CASE("test_option_with_choices") parser.option("--option") .choices({"value", "value1"}) .store(cfg); + parser.option("-a").flag(); parser.parse(); REQUIRE(cfg == "value"); @@ -134,6 +141,107 @@ TEST_CASE("test_option_with_choices_fail") parser.option("--option") .choices({"value", "value1"}) .store(cfg); - + REQUIRE_THROWS(parser.parse()); -} \ No newline at end of file +} + +TEST_CASE("test_option_positional") +{ + std::vector arguments{"", "file.txt"}; + clapp::ArgumentParser parser(arguments); + + auto& option = parser.option("SOME_FILE"); + parser.parse(); + + REQUIRE(option.value() == "file.txt"); +} + +TEST_CASE( + "test_option_positional_with_other_arguments_after_positional_argument") +{ + std::vector arguments{"", "file.txt", "-d", "1"}; + clapp::ArgumentParser parser(arguments); + + auto& positionalOption = parser.option("SOME_FILE"); + auto& intOption = parser.option("-d"); + parser.parse(); + + REQUIRE(positionalOption.value() == "file.txt"); + REQUIRE(intOption.value() == 1); +} + +TEST_CASE( + "test_option_positional_with_other_arguments_before_positional_argument") +{ + std::vector arguments{"", "-i", "1", "file.txt"}; + clapp::ArgumentParser parser(arguments); + + auto& positionalOption = parser.option("SOME_FILE"); + auto& intOption = parser.option("-i"); + parser.parse(); + + REQUIRE(positionalOption.value() == "file.txt"); + REQUIRE(intOption.value() == 1); +} + +TEST_CASE("test_option_multiple_positional") +{ + std::vector arguments{"", "fileIn.txt", "fileOut.txt"}; + clapp::ArgumentParser parser(arguments); + + auto& positionalOptionIn = parser.option("INPUT_FILE"); + auto& positionalOptionOut = parser.option("OUTPUT_FILE"); + parser.parse(); + + REQUIRE(positionalOptionIn.value() == "fileIn.txt"); + REQUIRE(positionalOptionOut.value() == "fileOut.txt"); +} + +TEST_CASE("test_option_multiple_positional_interleaved_with_other_arguments") +{ + std::vector arguments{ + "", "fileIn.txt", "-i1", "1", "fileOut.txt", "-i2", "2"}; + clapp::ArgumentParser parser(arguments); + + auto& positionalOptionIn = parser.option("INPUT_FILE"); + auto& positionalOptionOut = parser.option("OUTPUT_FILE"); + auto& intOption1 = parser.option("-i1"); + auto& intOption2 = parser.option("-i2"); + parser.parse(); + + REQUIRE(positionalOptionIn.value() == "fileIn.txt"); + REQUIRE(positionalOptionOut.value() == "fileOut.txt"); + REQUIRE(intOption1.value() == 1); + REQUIRE(intOption2.value() == 2); +} + +TEST_CASE("test_option_multiple_positional_with_required_arguments") +{ + std::vector arguments{"", "fileIn.txt"}; + clapp::ArgumentParser parser(arguments); + + parser.option("INPUT_FILE"); + parser.option("-i").required(); + + REQUIRE_THROWS(parser.parse()); +} + +TEST_CASE("test_option_multiple_positional_required") +{ + std::vector arguments{"", "-a", "test"}; + clapp::ArgumentParser parser(arguments); + + parser.option("INPUT_FILE").required(); + + REQUIRE_THROWS(parser.parse()); +} + +TEST_CASE("test_option_multiple_positional_required_empty") +{ + std::vector arguments{""}; + clapp::ArgumentParser parser(arguments); + + parser.option("INPUT_FILE").required(); + + REQUIRE_FALSE(parser.parse()); +}