From 3726f6840f6b8c59af33bc0e9e597c36289f45c6 Mon Sep 17 00:00:00 2001 From: Brett Date: Wed, 12 Feb 2025 02:54:22 -0500 Subject: [PATCH] starting arge parse --- CMakeLists.txt | 6 +- include/blt/parse/argparse_v2.h | 177 ++++++++++++++++++++++++++++++++ src/blt/parse/argparse_v2.cpp | 149 +++++++++++++++++++++++++++ tests/argparse_tests.cpp | 23 +++++ 4 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 include/blt/parse/argparse_v2.h create mode 100644 src/blt/parse/argparse_v2.cpp create mode 100644 tests/argparse_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f8afa32..f49d90d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,8 +148,9 @@ install(TARGETS ${BLT_TARGET} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -macro(blt_add_project name source type) +macro(blt_add_test name source type) + message("Adding project ${name} of type ${type}" DEBUG) project(${name}-${type}) add_executable(${name}-${type} ${source}) @@ -194,7 +195,8 @@ endmacro() if (${BUILD_TESTS}) message("Building tests for version ${BLT_VERSION}") - blt_add_project(blt-iterator tests/iterator_tests.cpp test) + blt_add_test(blt_iterator tests/iterator_tests.cpp test) + blt_add_test(blt_argparse tests/argparse_tests.cpp test) message("Built tests") endif () diff --git a/include/blt/parse/argparse_v2.h b/include/blt/parse/argparse_v2.h new file mode 100644 index 0000000..82ff1f0 --- /dev/null +++ b/include/blt/parse/argparse_v2.h @@ -0,0 +1,177 @@ +#pragma once +/* + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BLT_PARSE_ARGPARSE_V2_H +#define BLT_PARSE_ARGPARSE_V2_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace blt::argparse +{ + namespace detail + { + inline hashset_t allowed_flag_prefixes = {"-", "--", "+"}; + + std::string flag_prefixes_as_string(); + + inline std::string flag_prefix_list_string = flag_prefixes_as_string(); + + class bad_flag final : public std::runtime_error + { + public: + explicit bad_flag(const std::string& message): std::runtime_error(message) + { + } + + explicit bad_flag(const char* message): std::runtime_error(message) + { + } + }; + + template + struct arg_data_helper_t + { + using arg_primitive_data_t = std::variant; + using arg_list_data_t = std::variant...>; + }; + + using data_helper_t = arg_data_helper_t; + + using arg_primitive_data_t = data_helper_t::arg_primitive_data_t; + using arg_list_data_t = data_helper_t::arg_list_data_t; + using arg_data_t = std::variant; + + template + class arg_type_t; + + void test(); + } + + class argument_string_t + { + public: + explicit argument_string_t(const char* input): m_argument(input) + { + if (input == nullptr) + throw detail::bad_flag("Argument cannot be null!"); + if (m_argument.size() == 1) + throw detail::bad_flag("Argument cannot be a single character!"); + } + + [[nodiscard]] std::string_view get_flag() const + { + if (!flag_section) + process_argument(); + return *flag_section; + } + + [[nodiscard]] std::string_view get_name() const + { + if (!name_section) + process_argument(); + return *name_section; + } + + [[nodiscard]] std::string_view value() const + { + return get_name(); + } + + [[nodiscard]] bool is_flag() const + { + if (!m_is_flag) + process_argument(); + return *m_is_flag; + } + + [[nodiscard]] std::string_view get_argument() const + { + return m_argument; + } + + private: + void process_argument() const + { + size_t start = 0; + for (start = 0; start < m_argument.size(); ++start) + { + if (std::isalnum(m_argument[start])) + break; + } + m_is_flag = (start != 0); + flag_section = {m_argument.data(), start}; + name_section = {m_argument.data() + start, m_argument.size() - start}; + + if (!flag_section->empty() && !detail::allowed_flag_prefixes.contains(*flag_section)) + throw detail::bad_flag( + "Invalid flag detected, flag is not in allowed list of flags! Must be one of " + detail::flag_prefix_list_string); + } + + std::string_view m_argument; + mutable std::optional m_is_flag; + mutable std::optional flag_section; + mutable std::optional name_section; + }; + + class argument_consumer_t + { + public: + argument_consumer_t(const i32 argc, const char** argv): argv(argv), argc(argc) + { + } + + [[nodiscard]] std::string_view peek(const i32 offset = 0) const + { + return argv[forward_index + offset]; + } + + std::string_view consume() + { + return argv[forward_index++]; + } + + [[nodiscard]] i32 position() const + { + return argc; + } + + [[nodiscard]] i32 remaining() const + { + return argc - forward_index; + } + + [[nodiscard]] bool has_next(const i32 offset = 0) const + { + return (offset + forward_index) < argc; + } + + private: + const char** argv; + i32 argc; + i32 forward_index = 0; + }; +} + +#endif //BLT_PARSE_ARGPARSE_V2_H diff --git a/src/blt/parse/argparse_v2.cpp b/src/blt/parse/argparse_v2.cpp new file mode 100644 index 0000000..fcb89a0 --- /dev/null +++ b/src/blt/parse/argparse_v2.cpp @@ -0,0 +1,149 @@ +/* + * + * Copyright (C) 2025 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include + +namespace blt::argparse +{ + namespace detail + { + std::string flag_prefixes_as_string() + { + std::string result; + for (auto [i, v] : enumerate(allowed_flag_prefixes)) + { + result += '\''; + result += v; + result += '\''; + if (i != allowed_flag_prefixes.size() - 2) + result += ", "; + else if (i != allowed_flag_prefixes.size() - 1) + { + if (allowed_flag_prefixes.size() > 2) + result += ','; + result += " or "; + } + } + return result; + } + } + + + namespace detail + { + // Unit Tests for class argument_string_t + // Test Case 1: Ensure the constructor handles flags correctly + void test_argument_string_t_flag_basic() + { + const argument_string_t arg("-f"); + BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); + BLT_ASSERT(arg.value() == "-f" && "Flag value should match the input string."); + } + + // Test Case 2: Ensure the constructor handles long flags correctly + void test_argument_string_t_long_flag() + { + const argument_string_t arg("--file"); + BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); + BLT_ASSERT(arg.value() == "--file" && "Long flag value should match the input string."); + } + + // Test Case 3: Ensure positional arguments are correctly identified + void test_argument_string_t_positional_argument() + { + const argument_string_t arg("filename.txt"); + BLT_ASSERT(!arg.is_flag() && "Expected argument to be identified as positional."); + BLT_ASSERT(arg.value() == "filename.txt" && "Positional argument value should match the input string."); + } + + // Test Case 4: Test for an edge case where the string starts like a flag but isn't + void test_argument_string_t_invalid_flag() + { + const argument_string_t arg("-notFlagBecauseItIsPositional"); + BLT_ASSERT(!arg.is_flag() && "Expected argument to be identified as positional."); + BLT_ASSERT(arg.value() == "-notFlagBecauseItIsPositional" + && "Argument value should match the input string."); + } + + // Test Case 5: Handle an empty string + void test_argument_string_t_empty_input() + { + const argument_string_t arg(""); + BLT_ASSERT(!arg.is_flag() && "Expected an empty input to be treated as positional, not a flag."); + BLT_ASSERT(arg.value().empty() && "Empty input should have an empty value."); + } + + // Test Case 6: Handle edge case of a single hyphen (`-`) which might be ambiguous + void test_argument_string_t_single_hyphen() + { + const argument_string_t arg("-"); + BLT_ASSERT(arg.is_flag() && "Expected single hyphen (`-`) to be treated as a flag."); + BLT_ASSERT(arg.value() == "-" && "Single hyphen flag should match the input string."); + } + + // Test Case 7: Handle arguments with mixed cases + void test_argument_string_t_mixed_case() + { + const argument_string_t arg("-FlagWithMixedCASE"); + BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); + BLT_ASSERT(arg.value() == "-FlagWithMixedCASE" && "Mixed case flag value should match the input string."); + } + + // Test Case 8: Handle arguments with prefix only (like "--") + void test_argument_string_t_double_hyphen() + { + const argument_string_t arg("--"); + BLT_ASSERT(arg.is_flag() && "Double hyphen ('--') should be treated as a flag."); + BLT_ASSERT(arg.value() == "--" && "Double hyphen value should match the input string."); + } + + // Test Case 9: Validate edge case of an argument with spaces + void test_argument_string_t_with_spaces() + { + const argument_string_t arg(" "); + BLT_ASSERT(!arg.is_flag() && "Arguments with spaces should not be treated as flags."); + BLT_ASSERT(arg.value() == " " && "Arguments with spaces should match the input string."); + } + + // Test Case 10: Validate arguments with numeric characters + void test_argument_string_t_numeric_flag() + { + const argument_string_t arg("-123"); + BLT_ASSERT(arg.is_flag() && "Numeric flags should still be treated as flags."); + BLT_ASSERT(arg.value() == "-123" && "Numeric flag value should match the input string."); + } + + void run_all_tests_argument_string_t() + { + test_argument_string_t_flag_basic(); + test_argument_string_t_long_flag(); + test_argument_string_t_positional_argument(); + test_argument_string_t_invalid_flag(); + test_argument_string_t_empty_input(); + test_argument_string_t_single_hyphen(); + test_argument_string_t_mixed_case(); + test_argument_string_t_double_hyphen(); + test_argument_string_t_with_spaces(); + test_argument_string_t_numeric_flag(); + } + void test() + { + run_all_tests_argument_string_t(); + } + } +} diff --git a/tests/argparse_tests.cpp b/tests/argparse_tests.cpp new file mode 100644 index 0000000..124bfc6 --- /dev/null +++ b/tests/argparse_tests.cpp @@ -0,0 +1,23 @@ +/* + * + * Copyright (C) 2025 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include + +int main(){ + blt::argparse::detail::test(); + return 0; +} \ No newline at end of file