diff --git a/src/ArgParser.cpp b/src/ArgParser.cpp index c97d230..8f6f578 100644 --- a/src/ArgParser.cpp +++ b/src/ArgParser.cpp @@ -16,7 +16,7 @@ namespace YYCC::ArgParser { ArgumentList ArgumentList::CreateFromStd(int argc, char* argv[]) { std::vector args; - for (int i = 0; i < argc; ++i) { + for (int i = 1; i < argc; ++i) { // starts with 1 to remove first part (executable self) if (argv[i] != nullptr) args.emplace_back(yycc_u8string(YYCC::EncodingHelper::ToUTF8(argv[i]))); } @@ -34,7 +34,7 @@ namespace YYCC::ArgParser { int argc; LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); if (argv != NULL) { - for (int i = 0; i < argc; ++i) { + for (int i = 1; i < argc; ++i) { // starts with 1 to remove first part (executable self) if (argv[i] != nullptr) { yycc_u8string u8_argv; if (YYCC::EncodingHelper::WcharToUTF8(argv[i], u8_argv)) @@ -63,44 +63,63 @@ namespace YYCC::ArgParser { ++m_ArgumentsIterator; } - static bool IsLongName(const yycc_u8string& param, yycc_u8string_view* name_part) { - if (param.find(AbstractArgument::DOUBLE_DASH) != 0u) return false; - if (name_part != nullptr) - *name_part = yycc_u8string_view(param).substr(2u); - return true; + const yycc_u8string& ArgumentList::Current() const { + if (IsEOF()) throw std::runtime_error("attempt to get data on the tail of iterator."); + return *m_ArgumentsIterator; } - static bool IsShortName(const yycc_u8string& param, yycc_char8_t* name_part) { - if (param.size() != 2u || - param[0] != AbstractArgument::DASH || - param[1] == AbstractArgument::DASH || - param[1] < AbstractArgument::MIN_SHORT_NAME || param[1] > AbstractArgument::MAX_SHORT_NAME) { - return false; - } - if (name_part != nullptr) - *name_part = param[1]; - return true; - } - bool ArgumentList::IsSwitch( - bool* is_long_name = nullptr, - yycc_u8string_view* long_name = nullptr, - yycc_char8_t* short_name = nullptr) const { - // get argument first + + bool ArgumentList::IsSwitch(bool* is_long_name, yycc_u8string* long_name, yycc_char8_t* short_name) const { + // check eof first if (IsEOF()) throw std::runtime_error("attempt to fetch data on the tail of iterator."); - const auto& param = *m_ArgumentsIterator; // check long name first, then check short name - if (IsLongName(param, long_name)) { + if (IsLongNameSwitch(long_name)) { if (is_long_name != nullptr) *is_long_name = true; return true; } - if (IsShortName(param, short_name)) { + if (IsShortNameSwitch(short_name)) { if (is_long_name != nullptr) *is_long_name = false; return true; } // not matched return false; } + bool ArgumentList::IsLongNameSwitch(yycc_u8string* name_part) const { + // fetch current parameter + if (IsEOF()) throw std::runtime_error("attempt to fetch data on the tail of iterator."); + const yycc_u8string& param = *m_ArgumentsIterator; + // find double slash + if (param.find(AbstractArgument::DOUBLE_DASH) != 0u) return false; + // check gotten long name + yycc_u8string_view long_name = yycc_u8string_view(param).substr(2u); + if (!AbstractArgument::IsLegalLongName(long_name)) return false; + // set checked long name if possible and return + if (name_part != nullptr) + *name_part = long_name; + return true; + } + bool ArgumentList::IsShortNameSwitch(yycc_char8_t* name_part) const { + // fetch current parameter + if (IsEOF()) throw std::runtime_error("attempt to fetch data on the tail of iterator."); + const yycc_u8string& param = *m_ArgumentsIterator; + // if the length is not exactly equal to 2, + // or it not starts with dash, + // it is impossible a short name + if (param.size() != 2u || param[0] != AbstractArgument::DASH) return false; + // check gotten short name + yycc_char8_t short_name = param[1]; + if (!AbstractArgument::IsLegalShortName(short_name)) return false; + // set checked short name if possible and return + if (name_part != nullptr) + *name_part = short_name; + return true; + } - bool ArgumentList::IsValue() const { return !IsSwitch(); } + bool ArgumentList::IsValue(yycc_u8string* val) const { + bool is_value = !IsSwitch(); + if (is_value && val != nullptr) + *val = *m_ArgumentsIterator; + return is_value; + } bool ArgumentList::IsEOF() const { return m_ArgumentsIterator == m_Arguments.end(); } @@ -116,23 +135,46 @@ namespace YYCC::ArgParser { const yycc_char8_t AbstractArgument::MIN_SHORT_NAME = YYCC_U8_CHAR('!'); const yycc_char8_t AbstractArgument::MAX_SHORT_NAME = YYCC_U8_CHAR('~'); + bool AbstractArgument::IsLegalShortName(yycc_char8_t short_name) { + if (short_name == AbstractArgument::DASH || // dash is not allowed + short_name < AbstractArgument::MIN_SHORT_NAME || short_name > AbstractArgument::MAX_SHORT_NAME) { // non-display ASCII chars are not allowed + return false; + } + // okey + return true; + } + bool AbstractArgument::IsLegalLongName(const yycc_u8string_view& long_name) { + // empty is not allowed + if (long_name.empty()) return false; + // non-display ASCII chars are not allowed + for (const auto& val : long_name) { + if (val < AbstractArgument::MIN_SHORT_NAME || val > AbstractArgument::MAX_SHORT_NAME) + return false; + } + // okey + return true; + } + AbstractArgument::AbstractArgument( const yycc_char8_t* long_name, yycc_char8_t short_name, const yycc_char8_t* description, const yycc_char8_t* argument_example, bool is_optional) : - m_LongName(), m_ShortName(NO_SHORT_NAME), m_Description(), m_ArgumentExample(), + m_LongName(), m_ShortName(AbstractArgument::NO_SHORT_NAME), m_Description(), m_ArgumentExample(), m_IsOptional(is_optional), m_IsCaptured(false) { - // try to assign long name - if (long_name != nullptr) m_LongName = long_name; - // try to assign short name - if (short_name == AbstractArgument::DASH || - short_name < AbstractArgument::MIN_SHORT_NAME || - short_name > AbstractArgument::MAX_SHORT_NAME) { - throw std::invalid_argument("given short name character is invalid."); + // try to assign long name and check it + if (long_name != nullptr) { + m_LongName = long_name; + if (!AbstractArgument::IsLegalLongName(m_LongName)) + throw std::invalid_argument("Given long name is invalid."); } - m_ShortName = short_name; - // check short name and long name + // try to assign short name and check it + if (short_name != AbstractArgument::NO_SHORT_NAME) { + m_ShortName = short_name; + if (!AbstractArgument::IsLegalShortName(m_ShortName)) + throw std::invalid_argument("Given short name is invalid."); + } + // check short name and long name existence if (!HasShortName() && !HasLongName()) throw std::invalid_argument("you must specify an one of long name or short name."); @@ -187,8 +229,55 @@ namespace YYCC::ArgParser { OptionContext::~OptionContext() {} - bool OptionContext::Parse(ArgumentList* al) { - return false; //todo + bool OptionContext::Parse(ArgumentList& al) { + // reset argument list first + al.Reset(); + + // prepare variables and start loop + yycc_u8string long_name; + yycc_char8_t short_name; + bool is_long_name; + while (!al.IsEOF()) { + // if we can not find any switches, return with error + if (!al.IsSwitch(&is_long_name, &long_name, &short_name)) return false; + + // find corresponding argument by long name or short name. + // if we can not find it, return with error. + AbstractArgument* arg; + if (is_long_name) { + auto finder = m_LongNameMap.find(long_name); + if (finder == m_LongNameMap.end()) return false; + arg = finder->second; + } else { + auto finder = m_ShortNameMap.find(short_name); + if (finder == m_ShortNameMap.end()) return false; + arg = finder->second; + } + + // if this argument has been captured, raise error + if (arg->IsCaptured()) return false; + // call user parse function of found argument + if (arg->Parse(al)) { + // success. mark it is captured + arg->SetCaptured(true); + } else { + // failed, return error + return false; + } + + // move to next argument + al.Next(); + } + + // after processing all argument, + // we should check whether all non-optional argument are captured. + for (const auto* arg : m_Arguments) { + if (!arg->IsOptional() && !arg->IsCaptured()) + return false; + } + + // okey + return true; } void OptionContext::Reset() { diff --git a/src/ArgParser.hpp b/src/ArgParser.hpp index 6321fb2..e882994 100644 --- a/src/ArgParser.hpp +++ b/src/ArgParser.hpp @@ -3,6 +3,7 @@ #include "Constraints.hpp" #include "EncodingHelper.hpp" +#include "ParserHelper.hpp" #include #include #include @@ -18,17 +19,26 @@ namespace YYCC::ArgParser { #endif private: ArgumentList(std::vector&& arguments); + public: + ArgumentList(const ArgumentList&) = default; + ArgumentList& operator=(const ArgumentList&) = default; + ArgumentList(ArgumentList&&) = default; + ArgumentList& operator=(ArgumentList&&) = default; public: void Prev(); void Next(); + const yycc_u8string& Current() const; bool IsSwitch( bool* is_long_name = nullptr, - yycc_u8string_view* long_name = nullptr, + yycc_u8string* long_name = nullptr, yycc_char8_t* short_name = nullptr) const; - bool IsValue() const; + bool IsValue(yycc_u8string* val = nullptr) const; bool IsEOF() const; void Reset(); + private: + bool IsLongNameSwitch(yycc_u8string* name_part = nullptr) const; + bool IsShortNameSwitch(yycc_char8_t* name_part = nullptr) const; private: std::vector m_Arguments; @@ -37,12 +47,18 @@ namespace YYCC::ArgParser { class AbstractArgument { friend class OptionContext; + + // Long name and short name constants and checker. public: static const yycc_u8string DOUBLE_DASH; static const yycc_char8_t DASH; static const yycc_char8_t NO_SHORT_NAME; static const yycc_char8_t MIN_SHORT_NAME; static const yycc_char8_t MAX_SHORT_NAME; + static bool IsLegalShortName(yycc_char8_t short_name); + static bool IsLegalLongName(const yycc_u8string_view& long_name); + + // Constructor & destructor public: AbstractArgument( const yycc_char8_t* long_name, yycc_char8_t short_name = AbstractArgument::NO_SHORT_NAME, @@ -86,7 +102,7 @@ namespace YYCC::ArgParser { ~OptionContext(); public: - bool Parse(ArgumentList* al); + bool Parse(ArgumentList& al); void Reset(); private: @@ -118,8 +134,25 @@ namespace YYCC::ArgParser { } protected: - virtual bool Parse(ArgumentList& al) override {} // todo - virtual void Reset() override {}// todo + virtual bool Parse(ArgumentList& al) override { + // try get corresponding value + yycc_u8string strval; + al.Next(); + if (al.IsEOF() || !al.IsValue(&strval)) { + al.Prev(); + return false; + } + // try parsing value + if (!YYCC::ParserHelper::TryParse<_Ty>(strval, m_Data)) return false; + // check constraint + if (m_Constraint.IsValid() && !m_Constraint.m_CheckFct(m_Data)) + return false; + // okey + return true; + } + virtual void Reset() override { + std::memset(&m_Data, 0, sizeof(m_Data)); + } protected: _Ty m_Data; @@ -138,7 +171,10 @@ namespace YYCC::ArgParser { bool Get() const { return m_Data; } protected: - virtual bool Parse(ArgumentList& al) override { m_Data = true; } + virtual bool Parse(ArgumentList& al) override { + m_Data = true; + return true; + } virtual void Reset() override { m_Data = false; } protected: @@ -151,7 +187,7 @@ namespace YYCC::ArgParser { const yycc_char8_t* long_name, yycc_char8_t short_name, const yycc_char8_t* description = nullptr, const yycc_char8_t* argument_example = nullptr, bool is_optional = false, - Constraints::Constraint constraint = Constraints::Constraint {}) : + Constraints::Constraint constraint = Constraints::Constraint {}) : AbstractArgument(long_name, short_name, description, argument_example, is_optional), m_Data(), m_Constraint(constraint) {} virtual ~StringArgument() {} @@ -162,12 +198,26 @@ namespace YYCC::ArgParser { } protected: - virtual bool Parse(ArgumentList& al) override {} // todo - virtual void Reset() override {}// todo + virtual bool Parse(ArgumentList& al) override { + // try get corresponding value + al.Next(); + if (al.IsEOF() || !al.IsValue(&m_Data)) { + al.Prev(); + return false; + } + // check constraint + if (m_Constraint.IsValid() && !m_Constraint.m_CheckFct(m_Data)) + return false; + // okey + return true; + } + virtual void Reset() override { + m_Data.clear(); + } protected: yycc_u8string m_Data; - Constraints::Constraint m_Constraint; + Constraints::Constraint m_Constraint; }; #pragma endregion diff --git a/src/ConfigManager.hpp b/src/ConfigManager.hpp index f39edc0..b5ed94c 100644 --- a/src/ConfigManager.hpp +++ b/src/ConfigManager.hpp @@ -185,7 +185,7 @@ namespace YYCC::ConfigManager { */ StringSetting( const yycc_char8_t* name, const yycc_u8string_view& default_value, - Constraints::Constraint constraint = Constraints::Constraint {}) : + Constraints::Constraint constraint = Constraints::Constraint {}) : AbstractSetting(name), m_Data(), m_DefaultData(), m_Constraint(constraint) { m_Data = default_value; m_DefaultData = default_value; @@ -201,10 +201,11 @@ namespace YYCC::ConfigManager { */ bool Set(const yycc_u8string_view& new_data) { // check data validation - if (m_Constraint.IsValid() && !m_Constraint.m_CheckFct(new_data)) + yycc_u8string new_data_cache(new_data); + if (m_Constraint.IsValid() && !m_Constraint.m_CheckFct(new_data_cache)) return false; // assign data - m_Data = new_data; + m_Data = std::move(new_data_cache); return true; } @@ -244,7 +245,7 @@ namespace YYCC::ConfigManager { } yycc_u8string m_Data, m_DefaultData; - Constraints::Constraint m_Constraint; + Constraints::Constraint m_Constraint; }; #pragma endregion diff --git a/src/Constraints.hpp b/src/Constraints.hpp index 02323ba..d78961a 100644 --- a/src/Constraints.hpp +++ b/src/Constraints.hpp @@ -73,10 +73,10 @@ namespace YYCC::Constraints { * Caller must make sure that the string view passed in initializer list is valid until this Constraint life time gone. * Becasue this generator will not copy your given string view into string. */ - Constraint GetStringEnumerationConstraint(const std::initializer_list& il) { + inline Constraint GetStringEnumerationConstraint(const std::initializer_list& il) { std::set data(il); - return Constraint { - [data](const yycc_u8string_view& val) -> bool { return data.find(val) != data.end(); } + return Constraint { + [data](const yycc_u8string& val) -> bool { return data.find(yycc_u8string_view(val)) != data.end(); } }; } diff --git a/src/YYCCommonplace.hpp b/src/YYCCommonplace.hpp index 3a4f5a2..d4591e9 100644 --- a/src/YYCCommonplace.hpp +++ b/src/YYCCommonplace.hpp @@ -14,3 +14,4 @@ #include "ExceptionHelper.hpp" #include "ConfigManager.hpp" +#include "ArgParser.hpp" diff --git a/testbench/main.cpp b/testbench/main.cpp index ec26d5c..a9aaca8 100644 --- a/testbench/main.cpp +++ b/testbench/main.cpp @@ -391,12 +391,12 @@ namespace YYCCTestbench { } + enum class TestEnum : int8_t { + Test1, Test2, Test3 + }; + class TestConfigManager { public: - enum class TestEnum : int8_t { - Test1, Test2, Test3 - }; - TestConfigManager() : m_IntSetting(YYCC_U8("int-setting"), INT32_C(0)), m_FloatSetting(YYCC_U8("float-setting"), 0.0f), @@ -406,8 +406,7 @@ namespace YYCCTestbench { m_EnumSetting(YYCC_U8("enum-setting"), TestEnum::Test1), m_CoreManager(YYCC_U8("test.cfg"), UINT64_C(0), { &m_IntSetting, &m_FloatSetting, &m_StringSetting, &m_BoolSetting, &m_ClampedFloatSetting, &m_EnumSetting - }) - {} + }) {} ~TestConfigManager() {} void PrintSettings() { @@ -452,10 +451,10 @@ namespace YYCCTestbench { TEST_MACRO(m_StringSetting, YYCC_U8("fuck")); TEST_MACRO(m_BoolSetting, true); TEST_MACRO(m_ClampedFloatSetting, 0.5f); - TEST_MACRO(m_EnumSetting, TestConfigManager::TestEnum::Test2); + TEST_MACRO(m_EnumSetting, TestEnum::Test2); #undef TEST_MACRO - + // test save test.PrintSettings(); Assert(test.m_CoreManager.Save(), YYCC_U8("YYCC::ConfigManager::CoreManager::Save")); @@ -468,7 +467,7 @@ namespace YYCCTestbench { Assert(test.m_StringSetting.Get() == YYCC_U8(""), YYCC_U8("YYCC::ConfigManager::CoreManager::Reset")); Assert(test.m_BoolSetting.Get() == false, YYCC_U8("YYCC::ConfigManager::CoreManager::Reset")); Assert(test.m_ClampedFloatSetting.Get() == 0.0f, YYCC_U8("YYCC::ConfigManager::CoreManager::Reset")); - Assert(test.m_EnumSetting.Get() == TestConfigManager::TestEnum::Test1, YYCC_U8("YYCC::ConfigManager::CoreManager::Reset")); + Assert(test.m_EnumSetting.Get() == TestEnum::Test1, YYCC_U8("YYCC::ConfigManager::CoreManager::Reset")); // test load Assert(test.m_CoreManager.Load(), YYCC_U8("YYCC::ConfigManager::CoreManager::Load")); @@ -478,13 +477,117 @@ namespace YYCCTestbench { Assert(test.m_StringSetting.Get() == YYCC_U8("fuck"), YYCC_U8("YYCC::ConfigManager::CoreManager::Load")); Assert(test.m_BoolSetting.Get() == true, YYCC_U8("YYCC::ConfigManager::CoreManager::Load")); Assert(test.m_ClampedFloatSetting.Get() == 0.5f, YYCC_U8("YYCC::ConfigManager::CoreManager::Load")); - Assert(test.m_EnumSetting.Get() == TestConfigManager::TestEnum::Test2, YYCC_U8("YYCC::ConfigManager::CoreManager::Load")); - + Assert(test.m_EnumSetting.Get() == TestEnum::Test2, YYCC_U8("YYCC::ConfigManager::CoreManager::Load")); + + } + + class TestArgParser { + public: + TestArgParser() : + m_IntArgument(YYCC_U8("int"), YYCC_U8_CHAR('i'), YYCC_U8("integral argument"), YYCC_U8("114514")), + m_FloatArgument(nullptr, YYCC_U8_CHAR('f'), nullptr, nullptr, true), + m_StringArgument(YYCC_U8("string"), YYCC::ArgParser::AbstractArgument::NO_SHORT_NAME, nullptr, nullptr, true), + m_BoolArgument(nullptr, YYCC_U8_CHAR('b'), nullptr, nullptr), + m_ClampedFloatArgument(YYCC_U8("clamped-float"), YYCC::ArgParser::AbstractArgument::NO_SHORT_NAME, nullptr, nullptr, true, YYCC::Constraints::GetMinMaxRangeConstraint(-1.0f, 1.0f)), + m_OptionContext(YYCC_U8("TestArgParser"), YYCC_U8("This is the testbench of argument parser."), { + &m_IntArgument, &m_FloatArgument, &m_StringArgument, + &m_BoolArgument, &m_ClampedFloatArgument + }) {} + ~TestArgParser() {} + + YYCC::ArgParser::NumberArgument m_IntArgument; + YYCC::ArgParser::NumberArgument m_FloatArgument; + YYCC::ArgParser::StringArgument m_StringArgument; + + YYCC::ArgParser::SwitchArgument m_BoolArgument; + YYCC::ArgParser::NumberArgument m_ClampedFloatArgument; + + YYCC::ArgParser::OptionContext m_OptionContext; + }; + + static void ArgParserTestbench(int argc, char* argv[]) { + // test command line getter + { + YYCC::ConsoleHelper::WriteLine(YYCC_U8("YYCC::ArgParser::ArgumentList::CreateFromStd")); + auto result = YYCC::ArgParser::ArgumentList::CreateFromStd(argc, argv); + for (result.Reset(); !result.IsEOF(); result.Next()) { + YYCC::ConsoleHelper::FormatLine(YYCC_U8("\t%s"), result.Current().c_str()); + } + } +#if YYCC_OS == YYCC_OS_WINDOWS + { + YYCC::ConsoleHelper::WriteLine(YYCC_U8("YYCC::ArgParser::ArgumentList::CreateFromWin32")); + auto result = YYCC::ArgParser::ArgumentList::CreateFromWin32(); + for (result.Reset(); !result.IsEOF(); result.Next()) { + YYCC::ConsoleHelper::FormatLine(YYCC_U8("\t%s"), result.Current().c_str()); + } + } +#endif + + // test option context + // init option context + TestArgParser test; + +#define PREPARE_DATA(...) char* test_argv[] = { __VA_ARGS__ }; \ +auto al = YYCC::ArgParser::ArgumentList::CreateFromStd(sizeof(test_argv) / sizeof(char*), test_argv); + + // normal test + { + PREPARE_DATA("exec", "-i", "114514"); + Assert(test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(test.m_IntArgument.IsCaptured() && test.m_IntArgument.Get() == UINT32_C(114514), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(!test.m_BoolArgument.Get(), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + // no argument + { + PREPARE_DATA("exec"); + Assert(!test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + // error argument + { + PREPARE_DATA("exec", "-?", "114514"); + Assert(!test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + // lost argument + { + PREPARE_DATA("exec", "-i"); + Assert(!test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + // extra useless argument + { + PREPARE_DATA("exec", "-i", "114514" "1919810"); + Assert(!test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + // invalid clamp argument + { + PREPARE_DATA("exec", "-i", "114514", "--clamped-float", "114.0"); + Assert(!test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + // full argument + { + PREPARE_DATA("exec", "-i", "114514", "-f", "2.0", "--string", "fuck", "-b", "--clamped-float", "0.5"); + Assert(test.m_OptionContext.Parse(al), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(test.m_IntArgument.IsCaptured() && test.m_IntArgument.Get() == UINT32_C(114514), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(test.m_FloatArgument.IsCaptured() && test.m_FloatArgument.Get() == 2.0f, YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(test.m_StringArgument.IsCaptured() && test.m_StringArgument.Get() == YYCC_U8("fuck"), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(test.m_BoolArgument.IsCaptured() && test.m_BoolArgument.Get(), YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + Assert(test.m_ClampedFloatArgument.IsCaptured() && test.m_ClampedFloatArgument.Get() == 0.5f, YYCC_U8("YYCC::ArgParser::OptionContext::Parse")); + test.m_OptionContext.Reset(); + } + +#undef PREPARE_DATA + } } -int main(int argc, char** args) { +int main(int argc, char* argv[]) { // common testbench // normal YYCCTestbench::EncodingTestbench(); @@ -494,6 +597,7 @@ int main(int argc, char** args) { YYCCTestbench::FsPathPatch(); // advanced YYCCTestbench::ConfigManagerTestbench(); + YYCCTestbench::ArgParserTestbench(argc, argv); // testbench which may terminal app or ordering input YYCCTestbench::ConsoleTestbench();