This commit is contained in:
xxx 2025-11-16 20:43:43 +08:00
parent 929bf2de37
commit 9396c26989
14 changed files with 27949 additions and 69 deletions

View File

@ -1,83 +1,60 @@
# Copyright (c) 2014 Jarryd Beck
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
cmake_minimum_required(VERSION 3.5...3.19)
cmake_minimum_required(VERSION 3.10)
project(cxxoptspp VERSION 3.3.1 LANGUAGES CXX)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
include(cxxopts)
# C++ standard
seti(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Get the version of the library
cxxopts_getversion(VERSION)
# Include directory
include_directories(include)
project(cxxopts
VERSION "${VERSION}"
LANGUAGES CXX
# Find nlohmann/json
find_package(nlohmann_json 3.2.0 REQUIRED)
# Define library sources
set(CXXOPTSPP_SOURCES
src/cxxoptspp.cpp
src/dependency_engine.cpp
src/type_system.cpp
src/reversible_parser.cpp
)
set("PROJECT_DESCRIPTION" "A header-only lightweight C++ command line option parser")
set("PROJECT_HOMEPAGE_URL" "https://github.com/jarro2783/cxxopts")
# Create static library
add_library(cxxoptspp STATIC ${CXXOPTSPP_SOURCES})
# Must include after the project call due to GNUInstallDirs requiring a language be enabled (IE. CXX)
include(GNUInstallDirs)
# Create shared library
add_library(cxxoptspp_shared SHARED ${CXXOPTSPP_SOURCES})
set_target_properties(cxxoptspp_shared PROPERTIES OUTPUT_NAME cxxoptspp)
# Determine whether this is a standalone project or included by other projects
set(CXXOPTS_STANDALONE_PROJECT OFF)
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
set(CXXOPTS_STANDALONE_PROJECT ON)
endif()
# Link nlohmann/json
target_link_libraries(cxxoptspp PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(cxxoptspp_shared PRIVATE nlohmann_json::nlohmann_json)
# Establish the project options
option(CXXOPTS_BUILD_EXAMPLES "Set to ON to build examples" ${CXXOPTS_STANDALONE_PROJECT})
option(CXXOPTS_BUILD_TESTS "Set to ON to build tests" ${CXXOPTS_STANDALONE_PROJECT})
option(CXXOPTS_ENABLE_INSTALL "Generate the install target" ${CXXOPTS_STANDALONE_PROJECT})
option(CXXOPTS_ENABLE_WARNINGS "Add warnings to CMAKE_CXX_FLAGS" ${CXXOPTS_STANDALONE_PROJECT})
option(CXXOPTS_USE_UNICODE_HELP "Use ICU Unicode library" OFF)
# Include cxxopts.hpp from original library
file(GLOB CXXOPTS_HEADER "include/cxxopts/*.hpp")
if (CXXOPTS_STANDALONE_PROJECT)
cxxopts_set_cxx_standard()
endif()
# Installation
install(TARGETS cxxoptspp cxxoptspp_shared
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
if (CXXOPTS_ENABLE_WARNINGS)
cxxopts_enable_warnings()
endif()
install(FILES include/cxxoptspp.hpp DESTINATION include)
install(FILES ${CXXOPTS_HEADER} DESTINATION include/cxxopts)
add_library(cxxopts INTERFACE)
add_library(cxxopts::cxxopts ALIAS cxxopts)
add_subdirectory(include)
# Example application
add_executable(example_enhanced src/example_enhanced.cpp)
target_link_libraries(example_enhanced PRIVATE cxxoptspp nlohmann_json::nlohmann_json)
# Link against the ICU library when requested
if(CXXOPTS_USE_UNICODE_HELP)
cxxopts_use_unicode()
endif()
# Tests
add_subdirectory(test)
# Install cxxopts when requested by the user
if (CXXOPTS_ENABLE_INSTALL)
cxxopts_install_logic()
endif()
# Export library
export(TARGETS cxxoptspp cxxoptspp_shared FILE cxxoptsppTargets.cmake)
configure_file(cxxoptsppConfig.cmake.in cxxoptsppConfig.cmake @ONLY)
# Build examples when requested by the user
if (CXXOPTS_BUILD_EXAMPLES)
add_subdirectory(src)
endif()
# Enable testing when requested by the user
if (CXXOPTS_BUILD_TESTS)
enable_testing()
add_subdirectory(test)
endif()
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cxxoptsppConfig.cmake"
DESTINATION lib/cmake/cxxoptspp)
install(EXPORT cxxoptsppTargets DESTINATION lib/cmake/cxxoptspp)

53
CMakeLists.txt_enhanced Normal file
View File

@ -0,0 +1,53 @@
cmake_minimum_required(VERSION 3.10)
project(cxxoptspp VERSION 1.0.0 LANGUAGES CXX)
# C++ standard requirements
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Include directories
include_directories(include)
# Find JSON library (nlohmann/json)
find_package(nlohmann_json REQUIRED)
# Build cxxoptspp library
add_library(cxxoptspp SHARED
src/cxxoptspp.cpp
)
# Link dependencies
target_link_libraries(cxxoptspp PUBLIC
nlohmann_json::nlohmann_json
)
# Install library
install(TARGETS cxxoptspp
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
# Install header files
install(DIRECTORY include/
DESTINATION include
FILES_MATCHING PATTERN "*.hpp"
)
# Build original cxxopts (for backward compatibility)
add_library(cxxopts INTERFACE)
target_include_directories(cxxopts INTERFACE include)
# Build examples
add_executable(example src/example.cpp)
target_link_libraries(example cxxopts)
add_executable(example_enhanced src/example_enhanced.cpp)
target_link_libraries(example_enhanced cxxoptspp nlohmann_json::nlohmann_json)
# Build tests
enable_testing()
add_executable(tests test/main.cpp test/options.cpp)
target_link_libraries(tests cxxopts)
add_test(NAME unit_tests COMMAND tests)

163
DESIGN.md Normal file
View File

@ -0,0 +1,163 @@
# cxxopts++ Enhanced Design Document
## Overview
This document describes the enhanced design for cxxopts++ that adds support for:
1. Multi-level subcommands with dynamic option dependencies
2. Type system for automatic validation and conversion
3. Reversible parsing and state serialization
## 1. Multi-level Subcommands with Dynamic Option Dependencies
### Core Design
- **Command Hierarchy**: Each command can have child subcommands, forming a tree structure
- **Context Isolation**: Each command level has its own option set and parsing context
- **Option Inheritance**: Child commands inherit options from parent commands by default
- **Dependency Engine**: Evaluates boolean expressions to validate option dependencies
### Key Components
#### `Command` Class
- Represents a single command or subcommand
- Contains:
- Command name and description
- Parent command reference
- Child commands map
- Option set for this command level
- Dependency rules
- Help generation logic
#### `CommandParser` Class
- Recursive parser that traverses the command tree
- Maintains parsing context (current command, accumulated options)
- Handles option inheritance
- Validates dependencies after parsing
#### Dependency Expression Language
- Supports boolean operators: `&&`, `||`, `!`
- Parentheses for grouping: `( --a || --b ) && !--c`
- Operands are option names (with or without dashes)
- Evaluated using a recursive descent parser
### Parsing Flow
1. Start at root command
2. For each argument:
a. If it's a subcommand, switch context and continue parsing
b. If it's an option, parse it in current context
3. After all arguments parsed:
a. Merge inherited options
b. Evaluate all dependency rules
c. Generate errors for violated rules
## 2. Type System for Automatic Validation & Conversion
### Core Design
- **Type Erasure**: Uses `std::any` or custom type erasure to store values
- **Converter Concept**: Classes that implement `from_string()` and `to_string()` methods
- **Validator Concept**: Classes that implement `validate()` method
- **Composition**: Support for nested types (containers of custom types)
### Key Components
#### `Type` Base Class
- Abstract base for all custom types
- Methods:
- `virtual std::any from_string(const std::string&) const = 0;
- `virtual std::string to_string(const std::any&) const = 0;
- `virtual bool validate(const std::string&) const = 0;
#### `BasicType<T>` Template
- Implements `Type` for fundamental types
- Uses `std::istringstream`/`std::ostringstream` for conversion
#### `ContainerType<T>` Template
- Implements `Type` for container types
- Supports custom delimiters
- Validates each element individually
#### Example Custom Type
```cpp
class IPAddress : public Type {
public:
std::any from_string(const std::string& s) const override;
std::string to_string(const std::any& value) const override;
bool validate(const std::string& s) const override;
};
```
### Conversion Flow
1. String input -> split (for containers)
2. Validate each segment
3. Convert to target type
4. Store in type-erased container
## 3. Reversible Parsing & State Serialization
### Core Design
- **Parse Tree**: Records the exact sequence of parsed elements
- **Metadata Storage**: Stores original input strings and parsing context
- **Serialization Format**: JSON with structured metadata
- **Reconstruction**: Rebuilds command line from JSON representation
### Key Components
#### `ParseNode` Class
- Represents a single parsed element (option, value, subcommand)
- Contains:
- Element type (option, value, subcommand)
- Original string representation
- Position in command line
- Associated metadata
#### `ParseTree` Class
- Root node representing the full command line
- Tree structure matching command hierarchy
- Methods for serialization/deserialization
#### `Serializer` Class
- Converts `ParseTree` to JSON
- Handles all type conversions
- Preserves original input strings
#### `Deserializer` Class
- Converts JSON back to `ParseTree`
- Reconstructs command line arguments
### Serialization Format Example
```json
{
"command": "tool",
"subcommands": [
{
"command": "module",
"subcommands": [
{
"command": "subcmd",
"options": [
{
"name": "--opt",
"value": "123",
"original": "--opt=123",
"type": "int"
}
]
}
]
}
]
}
```
## Implementation Plan
1. **Base Command Structure**: Implement `Command` and `CommandParser` classes
2. **Dependency Engine**: Implement expression parser and evaluator
3. **Type System**: Implement type erasure and custom type support
4. **Container Types**: Add support for vectors, maps, etc.
5. **Serialization**: Implement `ParseTree` and JSON serialization
6. **Integration**: Merge with existing cxxopts infrastructure
7. **Testing**: Create comprehensive test cases for all features
## Compatibility
- The enhanced library will be backward compatible with existing cxxopts code
- New features will be opt-in by default
- All existing APIs will continue to work unchanged

View File

@ -0,0 +1,58 @@
# 选项继承与覆盖问题分析
## 当前实现
当前的选项继承通过 `merge_options` 函数实现,其逻辑是:
1. 创建当前命令的选项副本
2. 遍历从当前命令到根命令的所有父命令
3. 重建合并的选项(注释说明 "We don't merge options directly because cxxopts::Options doesn't support it"
## 问题
1. **覆盖逻辑不明确**:当子命令与父命令存在同名选项时,无法明确子命令选项是否应该覆盖父命令选项
2. **重复解析风险**:重建选项可能导致选项被多次解析
3. **优先级未定义**:解析结果可能与预期的选项覆盖逻辑不一致
## 分析
从代码来看,`merge_options` 函数并没有实际合并选项,因为 `cxxopts::Options` 不支持直接合并。它只是遍历了父命令,但没有将父命令的选项添加到合并后的选项中。
当前的解析流程是:
1. 创建当前命令的选项副本
2. 遍历父命令(但没有合并选项)
3. 解析剩余的参数
这意味着**只有当前命令的选项会被解析**,父命令的选项不会被继承。这可能不是预期的行为。
## 解决方案
需要重新实现选项继承逻辑,明确选项覆盖规则。
### 选项覆盖规则
1. **子命令选项优先**:如果子命令与父命令存在同名选项,则子命令选项覆盖父命令选项
2. **父命令选项默认**:如果子命令没有定义某个选项,则继承父命令的选项
### 实现思路
1. 从根命令到当前命令遍历所有命令
2. 将每个命令的选项添加到合并后的选项中
3. 后添加的选项会覆盖先添加的选项
### 修改代码
需要修改 `merge_options` 函数,将父命令的选项添加到合并后的选项中:
```cpp
void CommandParser::merge_options(const Command& cmd, cxxopts::Options& merged) const {
const Command* parent = cmd.parent();
if (parent) {
merge_options(*parent, merged);
// Get all options from parent
const auto& parent_opts = parent->options();
// We need to copy the options from parent to merged
// This requires access to the internal options map of cxxopts::Options
// However, cxxopts::Options doesn't expose this directly
// So we need to find another way to copy options
}
}
```
### 注意事项
由于 `cxxopts::Options` 没有提供直接访问内部选项的接口,可能需要修改 `cxxopts` 库或寻找其他解决方案。
## 结论
当前实现没有正确实现选项继承,导致父命令的选项不会被解析。需要修改实现以解决这个问题。

View File

@ -0,0 +1,34 @@
#include <cxxoptspp.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
try {
// Create root command
cxxoptspp::Command root_cmd("app", "Test application for dependency rules");
root_cmd.add_option("mode", "Operating mode", "default");
root_cmd.add_option("output", "Output file path", "out.txt");
root_cmd.add_option("verbose", "Enable verbose mode", false);
// Add a dependency rule: mode == file
root_cmd.add_dependency("--mode == file");
// Add another rule: if verbose is enabled, output must end with .log
root_cmd.add_dependency("--verbose && --output == *.log");
// Create parser and parse
cxxoptspp::CommandParser parser(root_cmd);
auto result = parser.parse(argc, argv);
// Print results
std::cout << "Command executed successfully!" << std::endl;
std::cout << "Mode: " << result["mode"].as<std::string>() << std::endl;
std::cout << "Output: " << result["output"].as<std::string>() << std::endl;
std::cout << "Verbose: " << result["verbose"].as<bool>() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -0,0 +1,44 @@
#include "cxxoptspp.hpp"
#include <iostream>
using namespace std;
int main(int argc, const char* argv[]) {
// Create root command
Command root("app", "Multilevel subcommands example");
root.add_option("--verbose", "Verbose mode", false, false);
root.add_option("--config", "Configuration file", string("config.json"), false);
// Create level 1 subcommand
auto sub1 = root.add_subcommand("sub1", "Level 1 subcommand");
sub1->add_option("--config", "Configuration file", string("sub1_config.json"), false);
sub1->add_option("--sub1-option", "Sub1 specific option", 123, false);
// Create level 2 subcommand
auto sub1_sub1 = sub1->add_subcommand("sub1", "Level 2 subcommand");
sub1_sub1->add_option("--verbose", "Verbose mode", true, false);
sub1_sub1->add_option("--sub1-sub1-option", "Sub1-sub1 specific option", string("test"), false);
// Create another level 1 subcommand
auto sub2 = root.add_subcommand("sub2", "Another level 1 subcommand");
sub2->add_option("--config", "Configuration file", string("sub2_config.json"), false);
// Parse arguments
CommandParser parser(root);
auto result = parser.parse(argc, argv);
// Print results
cout << "Current command: " << parser.current_command().name() << endl;
cout << "Verbose: " << result["verbose"].as<bool>() << endl;
cout << "Config: " << result["config"].as<string>() << endl;
if (result.count("sub1-option")) {
cout << "Sub1 option: " << result["sub1-option"].as<int>() << endl;
}
if (result.count("sub1-sub1-option")) {
cout << "Sub1-sub1 option: " << result["sub1-sub1-option"].as<string>() << endl;
}
return 0;
}

501
include/cxxoptspp.hpp Normal file
View File

@ -0,0 +1,501 @@
#ifndef CXXOPTSPP_HPP_INCLUDED
#define CXXOPTSPP_HPP_INCLUDED
#include <cxxopts.hpp>
#include <memory>
#include <map>
#include <vector>
#include <string>
#include <any>
#include <functional>
#include <regex>
#include <nlohmann/json.hpp>
namespace cxxoptspp {
using json = nlohmann::json;
// Forward declarations
class Command;
class CommandParser;
class Type;
class DependencyRule;
class ParseTree;
// ============================
// 1. Multi-level Subcommands
// ============================
class Command {
public:
Command(std::string name, std::string description)
: m_name(std::move(name)), m_description(std::move(description)), m_parent(nullptr), m_options(m_name, m_description) {}
// Subcommand management
std::shared_ptr<Command> add_subcommand(std::string name, std::string description = "");
std::shared_ptr<Command> get_subcommand(const std::string& name) const;
const CommandMap& subcommands() const;
// Option management with inheritance support
template <typename T>
Command& add_option(std::string long_option, std::string description, T defaultValue, bool required = false) {
// Add the option to the internal cxxopts::Options object
m_options.add_option("", std::move(long_option), std::move(description), cxxopts::value<T>(defaultValue)->default_value(std::to_string(defaultValue)), required);
// Save the option definition with type erasure
auto def = std::make_unique<OptionDefinition<T>>(std::move(long_option), std::move(description), defaultValue, required);
m_option_definitions.push_back(std::move(def));
return *this;
}
Command& add_option(std::string long_option, std::string description, std::string defaultValue, bool required = false) {
// Add the option to the internal cxxopts::Options object
m_options.add_option("", std::move(long_option), std::move(description), cxxopts::value<std::string>(defaultValue)->default_value('"' + defaultValue + '"'), required);
// Save the option definition with type erasure
auto def = std::make_unique<OptionDefinition<std::string>>(std::move(long_option), std::move(description), std::move(defaultValue), required);
m_option_definitions.push_back(std::move(def));
return *this;
}
void parse_positional(const std::string& option);
void parse_positional(std::vector<std::string> options);
// Getter for option definitions
const std::vector<OptionDefinition>& option_definitions() const;
// Dependency management
void add_dependency(std::string rule);
const DependencyList& dependencies() const;
// Getters
const std::string& name() const;
const std::string& description() const;
const cxxopts::Options& options() const;
cxxopts::Options& options();
const Command* parent() const;
// Getter for option definitions (for CommandParser only)
const std::vector<std::unique_ptr<OptionDefinitionBase>>& option_definitions() const;
private:
friend class CommandParser;
std::string m_name;
std::string m_description;
const Command* m_parent;
CommandMap m_subcommands;
cxxopts::Options m_options;
DependencyList m_dependencies;
// For option inheritance with type erasure
struct OptionDefinitionBase {
std::string option;
std::string description;
std::string defaultValue;
bool required;
OptionDefinitionBase(std::string option, std::string description, std::string defaultValue, bool required)
: option(std::move(option)), description(std::move(description)), defaultValue(std::move(defaultValue)), required(required) {}
virtual ~OptionDefinitionBase() = default;
virtual void add_to_options(cxxopts::Options& options) const = 0;
};
template <typename T>
struct OptionDefinition : public OptionDefinitionBase {
OptionDefinition(std::string option, std::string description, T defaultValue, bool required)
: OptionDefinitionBase(std::move(option), std::move(description), std::to_string(defaultValue), required), defaultValue(defaultValue) {}
void add_to_options(cxxopts::Options& options) const override {
T value = defaultValue;
options.add_option("", option, description, cxxopts::value<T>(value)->default_value(std::to_string(defaultValue)), required);
}
T defaultValue;
};
// Specialization for string type
template <>
struct OptionDefinition<std::string> : public OptionDefinitionBase {
OptionDefinition(std::string option, std::string description, std::string defaultValue, bool required)
: OptionDefinitionBase(std::move(option), std::move(description), defaultValue, required), defaultValue(std::move(defaultValue)) {}
void add_to_options(cxxopts::Options& options) const override {
std::string value = defaultValue;
options.add_option("", option, description, cxxopts::value<std::string>(value)->default_value('"' + defaultValue + '"'), required);
}
std::string defaultValue;
};
std::vector<std::unique_ptr<OptionDefinitionBase>> m_option_definitions;
};
class CommandParser {
public:
explicit CommandParser(Command& root_command);
cxxopts::ParseResult parse(int argc, const char* const argv[]);
cxxopts::ParseResult parse(const std::vector<std::string>& args);
const Command& current_command() const;
const cxxopts::ParseResult& result() const;
private:
struct ParseContext {
const Command* current_cmd;
std::vector<std::string> remaining_args;
cxxopts::Options merged_options;
std::vector<std::shared_ptr<DependencyRule>> merged_dependencies;
};
void merge_options(const Command& cmd, cxxopts::Options& merged) const;
void merge_dependencies(const Command& cmd, std::vector<std::shared_ptr<DependencyRule>>& merged) const;
void validate_dependencies(const cxxopts::ParseResult& result) const;
Command& m_root_command;
const Command* m_current_command;
cxxopts::ParseResult m_result;
};
// ============================
// 2. Type System
// ============================
class Type {
public:
virtual ~Type() = default;
virtual std::any from_string(const std::string& input) const = 0;
virtual std::string to_string(const std::any& value) const = 0;
virtual bool validate(const std::string& input) const = 0;
};
template <typename T>
class BasicType : public Type {
public:
std::any from_string(const std::string& input) const override {
T value;
std::istringstream iss(input);
iss >> value;
if (!iss.eof()) {
throw cxxopts::exceptions::incorrect_argument_type(input);
}
return value;
}
std::string to_string(const std::any& value) const override {
std::ostringstream oss;
oss << std::any_cast<const T&>(value);
return oss.str();
}
bool validate(const std::string& input) const override {
try {
from_string(input);
return true;
} catch (...) {
return false;
}
}
};
template <typename T>
class ContainerType : public Type {
public:
explicit ContainerType(char delimiter = ',') : m_delimiter(delimiter) {}
std::any from_string(const std::string& input) const override {
std::vector<T> result;
std::istringstream iss(input);
std::string token;
while (std::getline(iss, token, m_delimiter)) {
BasicType<T> element_type;
result.push_back(std::any_cast<T>(element_type.from_string(token)));
}
return result;
}
std::string to_string(const std::any& value) const override {
const auto& container = std::any_cast<const std::vector<T>&>(value);
std::ostringstream oss;
BasicType<T> element_type;
for (size_t i = 0; i < container.size(); ++i) {
if (i > 0) oss << m_delimiter;
oss << element_type.to_string(container[i]);
}
return oss.str();
}
bool validate(const std::string& input) const override {
try {
from_string(input);
return true;
} catch (...) {
return false;
}
}
private:
char m_delimiter;
};
// Example custom type: IP Address
class IPAddress {
public:
IPAddress() = default;
IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
: octets{a, b, c, d} {}
uint8_t octets[4];
std::string to_string() const {
std::ostringstream oss;
oss << static_cast<int>(octets[0]) << "."
<< static_cast<int>(octets[1]) << "."
<< static_cast<int>(octets[2]) << "."
<< static_cast<int>(octets[3]);
return oss.str();
}
bool operator==(const IPAddress& other) const {
return std::memcmp(octets, other.octets, 4) == 0;
}
};
// Specialize parse_value for IPAddress
template<>
inline void parse_value(const std::string& text, IPAddress& value) {
std::regex ip_regex(R"((\d+)\.(\d+)\.(\d+)\.(\d+))");
std::smatch match;
if (!std::regex_match(text, match, ip_regex)) {
throw cxxopts::exceptions::incorrect_argument_type(text);
}
for (int i = 0; i < 4; ++i) {
int octet = std::stoi(match[i+1]);
if (octet < 0 || octet > 255) {
throw cxxopts::exceptions::incorrect_argument_type(text);
}
value.octets[i] = static_cast<uint8_t>(octet);
}
}
// Specialize parse_value for vector<IPAddress>
template<>
inline void parse_value(const std::string& text, std::vector<IPAddress>& value) {
if (text.empty()) {
return;
}
std::stringstream in(text);
std::string token;
while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) {
IPAddress ip;
parse_value(token, ip);
value.emplace_back(std::move(ip));
}
}
class IPAddressType : public Type {
public:
std::any from_string(const std::string& input) const override {
return IPAddress::from_string(input);
}
std::string to_string(const std::any& value) const override {
return std::any_cast<const IPAddress&>(value).to_string();
}
bool validate(const std::string& input) const override {
try {
IPAddress::from_string(input);
return true;
} catch (...) {
return false;
}
}
};
// ============================
// 3. Dependency Rules Engine
// ============================
class DependencyRule {
public:
virtual ~DependencyRule() = default;
virtual bool evaluate(const cxxopts::ParseResult& result) const = 0;
virtual std::string error_message() const = 0;
};
class SimpleDependencyRule : public DependencyRule {
public:
explicit SimpleDependencyRule(std::string rule_str);
bool evaluate(const cxxopts::ParseResult& result) const override;
std::string error_message() const override;
private:
enum class TokenType { AND, OR, NOT, LPAREN, RPAREN, OPTION, END };
struct Token {
TokenType type;
std::string value;
};
std::vector<Token> tokenize(const std::string& rule) const;
std::vector<Token> infix_to_postfix(const std::vector<Token>& infix) const;
bool evaluate_postfix(const std::vector<Token>& postfix, const cxxopts::ParseResult& result) const;
std::string m_rule_str;
std::vector<Token> m_postfix;
std::string m_error_msg;
};
// ============================
// 4. Reversible Parsing & Serialization
// ============================
class ParseNode {
public:
enum class NodeType { ROOT, COMMAND, OPTION, VALUE, POSITIONAL };
ParseNode(NodeType type, std::string value, const Command* cmd = nullptr)
: m_type(type), m_value(std::move(value)), m_command(cmd) {}
// Add child node
void add_child(std::shared_ptr<ParseNode> child) {
m_children.push_back(std::move(child));
}
// Serialization
json to_json() const;
// Deserialization
static std::shared_ptr<ParseNode> from_json(const json& j, const Command* root_cmd);
// Reconstruct command line
std::vector<std::string> to_command_line() const;
private:
NodeType m_type;
std::string m_value;
const Command* m_command;
std::vector<std::shared_ptr<ParseNode>> m_children;
};
class ParseTree {
public:
ParseTree() : m_root(std::make_shared<ParseNode>(ParseNode::NodeType::ROOT, "")) {}
// Add root command
void set_root_command(std::shared_ptr<Command> cmd) {
m_root->add_child(std::make_shared<ParseNode>(ParseNode::NodeType::COMMAND, cmd->name(), cmd.get()));
m_current_node = m_root->m_children.back().get();
}
// Add subcommand
void add_subcommand(std::shared_ptr<Command> cmd) {
auto child = std::make_shared<ParseNode>(ParseNode::NodeType::COMMAND, cmd->name(), cmd.get());
m_current_node->add_child(std::move(child));
m_current_node = m_current_node->m_children.back().get();
}
// Add option
void add_option(const std::string& option_str, const Command* cmd = nullptr) {
m_current_node->add_child(std::make_shared<ParseNode>(ParseNode::NodeType::OPTION, option_str, cmd));
}
// Add value
void add_value(const std::string& value_str, const Command* cmd = nullptr) {
m_current_node->add_child(std::make_shared<ParseNode>(ParseNode::NodeType::VALUE, value_str, cmd));
}
// Serialization
json to_json() const { return m_root->to_json(); }
// Deserialization
static ParseTree from_json(const json& j, const Command* root_cmd);
// Reconstruct command line
std::vector<std::string> to_command_line() const { return m_root->to_command_line(); }
private:
std::shared_ptr<ParseNode> m_root;
ParseNode* m_current_node;
};
// Helper functions for option creation with custom types
template <typename T>
std::shared_ptr<cxxopts::Value> value() {
return std::make_shared<cxxopts::values::standard_value<T>>();
}
template <typename T>
std::shared_ptr<cxxopts::Value> value(T& t) {
return std::make_shared<cxxopts::values::standard_value<T>>(&t);
}
// Custom value with type validation
template <typename T>
class TypedValue : public cxxopts::Value {
public:
TypedValue() : m_result(std::make_shared<T>()), m_store(m_result.get()) {}
explicit TypedValue(T* t) : m_store(t) {}
void add(const std::string& text) const override {
// Not implemented for custom types
throw cxxopts::exceptions::option_has_no_value("TypedValue");
}
void parse(const std::string& text) const override {
BasicType<T> type;
*m_store = std::any_cast<T>(type.from_string(text));
}
bool is_container() const override { return false; }
void parse() const override { /* no default */ }
bool has_default() const override { return false; }
bool has_implicit() const override { return false; }
std::shared_ptr<cxxopts::Value> default_value(const std::string&) override { return shared_from_this(); }
std::shared_ptr<cxxopts::Value> implicit_value(const std::string&) override { return shared_from_this(); }
std::shared_ptr<cxxopts::Value> no_implicit_value() override { return shared_from_this(); }
std::string get_default_value() const override { return ""; }
std::string get_implicit_value() const override { return ""; }
bool is_boolean() const override { return false; }
std::shared_ptr<cxxopts::Value> clone() const override {
return std::make_shared<TypedValue<T>>(*this);
}
const T& get() const { return *m_store; }
private:
std::shared_ptr<T> m_result;
T* m_store;
};
// Export to namespace cxxopts for backward compatibility
namespace cxxopts {
using cxxoptspp::Command;
using cxxoptspp::CommandParser;
using cxxoptspp::Type;
using cxxoptspp::BasicType;
using cxxoptspp::ContainerType;
using cxxoptspp::IPAddress;
using cxxoptspp::IPAddressType;
using cxxoptspp::DependencyRule;
using cxxoptspp::SimpleDependencyRule;
using cxxoptspp::ParseTree;
using cxxoptspp::TypedValue;
}
} // namespace cxxoptspp
#endif // CXXOPTSPP_HPP_INCLUDED

25526
include/json.hpp Normal file

File diff suppressed because it is too large Load Diff

411
src/cxxoptspp.cpp Normal file
View File

@ -0,0 +1,411 @@
#include <cxxoptspp.hpp>
#include <sstream>
#include <stdexcept>
#include <stack>
#include <algorithm>
namespace cxxoptspp {
// ============================
// Command Implementation
// ============================
Command::Command(std::string name, std::string description)
: m_name(std::move(name)),
m_description(std::move(description)),
m_parent(nullptr),
m_options(m_name)
{}
std::shared_ptr<Command> Command::add_subcommand(std::string name, std::string description) {
auto cmd = std::make_shared<Command>(std::move(name), std::move(description));
cmd->m_parent = this;
m_subcommands[cmd->m_name] = cmd;
return cmd;
}
std::shared_ptr<Command> Command::get_subcommand(const std::string& name) const {
auto it = m_subcommands.find(name);
return it != m_subcommands.end() ? it->second : nullptr;
}
const Command::CommandMap& Command::subcommands() const {
return m_subcommands;
}
void Command::parse_positional(const std::string& option) {
m_options.parse_positional(option);
}
void Command::parse_positional(std::vector<std::string> options) {
m_options.parse_positional(std::move(options));
}
void Command::add_dependency(std::string rule) {
m_dependencies.push_back(std::make_shared<SimpleDependencyRule>(std::move(rule)));
}
const Command::DependencyList& Command::dependencies() const {
return m_dependencies;
}
const std::string& Command::name() const {
return m_name;
}
const std::string& Command::description() const {
return m_description;
}
const cxxopts::Options& Command::options() const {
return m_options;
}
cxxopts::Options& Command::options() {
return m_options;
}
const Command* Command::parent() const {
return m_parent;
}
const std::vector<std::unique_ptr<Command::OptionDefinitionBase>>& Command::option_definitions() const {
return m_option_definitions;
}
// ============================
// CommandParser Implementation
// ============================
CommandParser::CommandParser(Command& root_command)
: m_root_command(root_command),
m_current_command(&root_command)
{}
cxxopts::ParseResult CommandParser::parse(int argc, const char* const argv[]) {
return parse(std::vector<std::string>(argv, argv + argc));
}
cxxopts::ParseResult CommandParser::parse(const std::vector<std::string>& args) {
if (args.empty()) {
m_result = cxxopts::ParseResult(std::make_shared<cxxopts::OptionMap>());
return m_result;
}
std::vector<std::string> remaining_args(args.begin() + 1, args.end()); // skip program name
m_current_command = &m_root_command;
// Traverse command tree
while (!remaining_args.empty()) {
const auto& arg = remaining_args.front();
auto subcmd = m_current_command->get_subcommand(arg);
if (subcmd) {
m_current_command = subcmd.get();
remaining_args.erase(remaining_args.begin());
} else {
break; // found an option, start parsing
}
}
// Merge options from all ancestor commands (root to current) with proper inheritance
// Create a new Options object with the current command's name and description
cxxopts::Options merged_options(m_current_command->name(), m_current_command->description());
// Collect all commands from root to current (inclusive)
std::vector<const Command*> command_chain;
const Command* current = m_current_command;
while (current) {
command_chain.insert(command_chain.begin(), current); // Insert at front to get root first
current = current->parent();
}
// Add options from all commands in the chain (root to current)
// This ensures that child command options override parent command options
for (const Command* cmd : command_chain) {
for (const auto& option_def : cmd->option_definitions()) {
option_def->add_to_options(merged_options);
}
}
// Merge dependencies from all parent commands
std::vector<std::shared_ptr<DependencyRule>> merged_deps = m_current_command->dependencies();
merge_dependencies(*m_current_command, merged_deps);
// Parse the remaining arguments
m_result = merged_options.parse(remaining_args);
// Validate dependencies
validate_dependencies(m_result);
return m_result;
}
const Command& CommandParser::current_command() const {
return *m_current_command;
}
const cxxopts::ParseResult& CommandParser::result() const {
return m_result;
}
void CommandParser::merge_options(const Command& cmd, cxxopts::Options& merged) const {
const Command* parent = cmd.parent();
if (parent) {
merge_options(*parent, merged);
// Workaround: Create a new Options object for the parent and parse an empty vector
// This will populate the option map with default values
cxxopts::Options parent_options = parent->options();
parent_options.parse({});
// Note: We can't directly copy options because cxxopts::Options doesn't support it
// Instead, we rely on the fact that we've already traversed from root to current command
// This ensures that the merged options will have the correct priority (child options override parent options)
}
}
void CommandParser::merge_dependencies(const Command& cmd, std::vector<std::shared_ptr<DependencyRule>>& merged) const {
const Command* parent = cmd.parent();
if (parent) {
auto parent_deps = parent->dependencies();
merged.insert(merged.begin(), parent_deps.begin(), parent_deps.end());
merge_dependencies(*parent, merged);
}
}
void CommandParser::validate_dependencies(const cxxopts::ParseResult& result) const {
// Check all merged dependencies
const Command* cmd = m_current_command;
while (cmd) {
for (const auto& dep : cmd->dependencies()) {
if (!dep->evaluate(result)) {
throw cxxopts::exceptions::parsing(dep->error_message());
}
}
cmd = cmd->parent();
}
}
// ============================
// SimpleDependencyRule Implementation
// ============================
SimpleDependencyRule::SimpleDependencyRule(std::string rule_str)
: m_rule_str(std::move(rule_str)) {
auto tokens = tokenize(m_rule_str);
m_postfix = infix_to_postfix(tokens);
m_error_msg = "Dependency violation: " + m_rule_str;
}
bool SimpleDependencyRule::evaluate(const cxxopts::ParseResult& result) const {
return evaluate_postfix(m_postfix, result);
}
std::string SimpleDependencyRule::error_message() const {
return m_error_msg;
}
std::vector<SimpleDependencyRule::Token> SimpleDependencyRule::tokenize(const std::string& rule) const {
std::vector<Token> tokens;
std::istringstream iss(rule);
std::string token;
while (iss >> token) {
if (token == "&&") {
tokens.push_back({TokenType::AND, token});
} else if (token == "||") {
tokens.push_back({TokenType::OR, token});
} else if (token == "!") {
tokens.push_back({TokenType::NOT, token});
} else if (token == "(") {
tokens.push_back({TokenType::LPAREN, token});
} else if (token == ")") {
tokens.push_back({TokenType::RPAREN, token});
} else {
// Option name - remove leading dashes if present
std::string opt_name = token;
if (opt_name.starts_with("--")) {
opt_name = opt_name.substr(2);
} else if (opt_name.starts_with("-")) {
opt_name = opt_name.substr(1);
}
tokens.push_back({TokenType::OPTION, opt_name});
}
}
tokens.push_back({TokenType::END, ""});
return tokens;
}
std::vector<SimpleDependencyRule::Token> SimpleDependencyRule::infix_to_postfix(const std::vector<Token>& infix) const {
std::vector<Token> postfix;
std::stack<Token> operators;
auto precedence = [](TokenType type) -> int {
if (type == TokenType::NOT) return 3;
if (type == TokenType::AND) return 2;
if (type == TokenType::OR) return 1;
return 0;
};
for (const auto& token : infix) {
switch (token.type) {
case TokenType::OPTION:
postfix.push_back(token);
break;
case TokenType::LPAREN:
operators.push(token);
break;
case TokenType::RPAREN:
while (!operators.empty() && operators.top().type != TokenType::LPAREN) {
postfix.push_back(operators.top());
operators.pop();
}
if (!operators.empty()) operators.pop(); // pop '('
break;
case TokenType::AND:
case TokenType::OR:
case TokenType::NOT:
while (!operators.empty() && precedence(operators.top().type) >= precedence(token.type)) {
postfix.push_back(operators.top());
operators.pop();
}
operators.push(token);
break;
case TokenType::END:
break;
}
}
while (!operators.empty()) {
postfix.push_back(operators.top());
operators.pop();
}
return postfix;
}
bool SimpleDependencyRule::evaluate_postfix(const std::vector<Token>& postfix, const cxxopts::ParseResult& result) const {
std::stack<bool> values;
for (const auto& token : postfix) {
switch (token.type) {
case TokenType::OPTION:
values.push(result.count(token.value) > 0);
break;
case TokenType::AND:
{
bool rhs = values.top(); values.pop();
bool lhs = values.top(); values.pop();
values.push(lhs && rhs);
break;
}
case TokenType::OR:
{
bool rhs = values.top(); values.pop();
bool lhs = values.top(); values.pop();
values.push(lhs || rhs);
break;
}
case TokenType::NOT:
{
bool val = values.top(); values.pop();
values.push(!val);
break;
}
default:
break;
}
}
return !values.empty() && values.top();
}
// ============================
// ParseNode Implementation
// ============================
json ParseNode::to_json() const {
json j;
j["type"] = static_cast<int>(m_type);
j["value"] = m_value;
if (m_command) {
j["command_name"] = m_command->name();
}
if (!m_children.empty()) {
json children;
for (const auto& child : m_children) {
children.push_back(child->to_json());
}
j["children"] = children;
}
return j;
}
std::shared_ptr<ParseNode> ParseNode::from_json(const json& j, const Command* root_cmd) {
int type_int = j.at("type").get<int>();
NodeType type = static_cast<NodeType>(type_int);
std::string value = j.at("value").get<std::string>();
// Find command if specified
const Command* cmd = nullptr;
if (j.contains("command_name")) {
std::string cmd_name = j.at("command_name").get<std::string>();
// For simplicity, we only set root command
if (root_cmd && root_cmd->name() == cmd_name) {
cmd = root_cmd;
}
// TODO: Handle nested commands
}
auto node = std::make_shared<ParseNode>(type, value, cmd);
if (j.contains("children")) {
for (const auto& child_j : j.at("children")) {
node->add_child(from_json(child_j, root_cmd));
}
}
return node;
}
std::vector<std::string> ParseNode::to_command_line() const {
std::vector<std::string> args;
switch (m_type) {
case NodeType::ROOT:
break;
case NodeType::COMMAND:
case NodeType::OPTION:
args.push_back(m_value);
break;
case NodeType::VALUE:
args.push_back(m_value);
break;
case NodeType::POSITIONAL:
args.push_back(m_value);
break;
}
for (const auto& child : m_children) {
auto child_args = child->to_command_line();
args.insert(args.end(), child_args.begin(), child_args.end());
}
return args;
}
// ============================
// ParseTree Implementation
// ============================
ParseTree ParseTree::from_json(const json& j, const Command* root_cmd) {
ParseTree tree;
auto root_node = ParseNode::from_json(j, root_cmd);
tree.m_root = std::move(root_node);
tree.m_current_node = tree.m_root.get();
return tree;
}
} // namespace cxxoptspp

335
src/dependency_engine.cpp Normal file
View File

@ -0,0 +1,335 @@
#include <cxxoptspp.hpp>
#include <sstream>
#include <stdexcept>
#include <stack>
#include <algorithm>
#include <regex>
namespace cxxoptspp {
// Enhanced DependencyRule with value comparison support
class EnhancedDependencyRule : public DependencyRule {
public:
explicit EnhancedDependencyRule(std::string rule_str)
: m_rule_str(std::move(rule_str)) {
auto tokens = tokenize(m_rule_str);
m_postfix = infix_to_postfix(tokens);
m_error_msg = "Dependency violation: " + m_rule_str;
}
bool evaluate(const cxxopts::ParseResult& result) const override {
return evaluate_postfix(m_postfix, result);
}
std::string error_message() const override {
return m_error_msg;
}
private:
enum class TokenType {
AND, OR, NOT,
LPAREN, RPAREN,
OPTION,
EQ, NE, GT, LT, GE, LE,
VALUE,
END
};
struct Token {
TokenType type;
std::string value;
};
std::vector<Token> tokenize(const std::string& rule) const {
std::vector<Token> tokens;
std::string input = rule;
// Handle operators with two characters first
std::vector<std::pair<std::string, TokenType>> operators = {
{"&&", TokenType::AND},
{"||", TokenType::OR},
{"==", TokenType::EQ},
{"!=", TokenType::NE},
{">=", TokenType::GE},
{"<=", TokenType::LE},
{">", TokenType::GT},
{"<", TokenType::LT},
{"!", TokenType::NOT},
{"(", TokenType::LPAREN},
{")", TokenType::RPAREN}
};
size_t pos = 0;
while (pos < input.size()) {
// Skip whitespace
while (pos < input.size() && std::isspace(input[pos])) {
++pos;
}
if (pos >= input.size()) break;
bool found = false;
// Check for operators
for (const auto& [op_str, op_type] : operators) {
if (input.substr(pos, op_str.size()) == op_str) {
tokens.push_back({op_type, op_str});
pos += op_str.size();
found = true;
break;
}
}
if (found) continue;
// Check for quoted values
if (input[pos] == '"' || input[pos] == "'") {
char quote = input[pos++];
size_t end_pos = input.find(quote, pos);
if (end_pos == std::string::npos) {
throw std::invalid_argument("Unclosed quote in rule: " + rule);
}
std::string val = input.substr(pos, end_pos - pos);
tokens.push_back({TokenType::VALUE, val});
pos = end_pos + 1;
continue;
}
// Check for options or values
size_t end_pos = pos;
while (end_pos < input.size() &&
!std::isspace(input[end_pos]) &&
input[end_pos] != '"' && input[end_pos] != "'" &&
input.substr(end_pos, 2) != "&&" && input.substr(end_pos, 2) != "||" &&
input.substr(end_pos, 2) != "==" && input.substr(end_pos, 2) != "!=" &&
input.substr(end_pos, 2) != ">=" && input.substr(end_pos, 2) != "<=" &&
input[end_pos] != '>' && input[end_pos] != '<' &&
input[end_pos] != '!' && input[end_pos] != '(' && input[end_pos] != ')') {
++end_pos;
}
std::string token_str = input.substr(pos, end_pos - pos);
if (!token_str.empty()) {
// Check if it's an option (starts with -- or -)
if (token_str.starts_with("--")) {
tokens.push_back({TokenType::OPTION, token_str.substr(2)});
} else if (token_str.starts_with("-")) {
tokens.push_back({TokenType::OPTION, token_str.substr(1)});
} else {
// Assume it's a value
tokens.push_back({TokenType::VALUE, token_str});
}
}
pos = end_pos;
}
tokens.push_back({TokenType::END, ""});
return tokens;
}
std::vector<Token> infix_to_postfix(const std::vector<Token>& infix) const {
std::vector<Token> postfix;
std::stack<Token> operators;
auto precedence = [](TokenType type) -> int {
if (type == TokenType::NOT) return 5;
if (type == TokenType::GT || type == TokenType::LT || type == TokenType::GE || type == TokenType::LE) return 4;
if (type == TokenType::EQ || type == TokenType::NE) return 3;
if (type == TokenType::AND) return 2;
if (type == TokenType::OR) return 1;
return 0;
};
for (const auto& token : infix) {
switch (token.type) {
case TokenType::OPTION:
case TokenType::VALUE:
postfix.push_back(token);
break;
case TokenType::LPAREN:
operators.push(token);
break;
case TokenType::RPAREN:
while (!operators.empty() && operators.top().type != TokenType::LPAREN) {
postfix.push_back(operators.top());
operators.pop();
}
if (!operators.empty()) operators.pop(); // pop '('
break;
case TokenType::AND:
case TokenType::OR:
case TokenType::NOT:
case TokenType::EQ:
case TokenType::NE:
case TokenType::GT:
case TokenType::LT:
case TokenType::GE:
case TokenType::LE:
while (!operators.empty() && precedence(operators.top().type) >= precedence(token.type)) {
postfix.push_back(operators.top());
operators.pop();
}
operators.push(token);
break;
case TokenType::END:
break;
}
}
while (!operators.empty()) {
postfix.push_back(operators.top());
operators.pop();
}
return postfix;
}
bool evaluate_postfix(const std::vector<Token>& postfix, const cxxopts::ParseResult& result) const {
// Stack holds either booleans (for existence checks) or strings (for option names/values)
std::stack<std::variant<bool, std::string>> values;
for (const auto& token : postfix) {
switch (token.type) {
case TokenType::OPTION:
{
// For options, we need to store the name to retrieve the value later if needed
values.push(token.value);
break;
}
case TokenType::VALUE:
{
values.push(token.value);
break;
}
case TokenType::AND:
case TokenType::OR:
case TokenType::NOT:
{
// For logical operations, we need boolean values
// Convert any string option names to existence booleans
auto convert_to_bool = [&result](std::variant<bool, std::string>& var) -> bool {
if (std::holds_alternative<bool>(var)) {
return std::get<bool>(var);
}
const std::string& opt_name = std::get<std::string>(var);
return result.count(opt_name) > 0;
};
if (token.type == TokenType::NOT) {
auto val_var = values.top(); values.pop();
bool val = convert_to_bool(val_var);
values.push(!val);
} else {
auto rhs_var = values.top(); values.pop();
auto lhs_var = values.top(); values.pop();
bool lhs = convert_to_bool(lhs_var);
bool rhs = convert_to_bool(rhs_var);
if (token.type == TokenType::AND) {
values.push(lhs && rhs);
} else { // OR
values.push(lhs || rhs);
}
}
break;
}
case TokenType::EQ:
case TokenType::NE:
case TokenType::GT:
case TokenType::LT:
case TokenType::GE:
case TokenType::LE:
{
auto rhs_var = values.top(); values.pop();
auto lhs_var = values.top(); values.pop();
std::string lhs_val, rhs_val;
// Resolve lhs - could be an option name or a value
if (std::holds_alternative<std::string>(lhs_var)) {
const std::string& lhs_str = std::get<std::string>(lhs_var);
// Check if it's an option (exists in result)
if (result.count(lhs_str) > 0) {
// It's an option - get its value from the result
lhs_val = result[lhs_str].as<std::string>();
} else {
// It's a literal value
lhs_val = lhs_str;
}
} else {
// This should not happen for comparison operations
values.push(false);
break;
}
// Resolve rhs - could be an option name or a value
if (std::holds_alternative<std::string>(rhs_var)) {
const std::string& rhs_str = std::get<std::string>(rhs_var);
// Check if it's an option (exists in result)
if (result.count(rhs_str) > 0) {
// It's an option - get its value from the result
rhs_val = result[rhs_str].as<std::string>();
} else {
// It's a literal value
rhs_val = rhs_str;
}
} else {
// This should not happen for comparison operations
values.push(false);
break;
}
// Perform the appropriate comparison
bool result_val = false;
switch (token.type) {
case TokenType::EQ:
result_val = (lhs_val == rhs_val);
break;
case TokenType::NE:
result_val = (lhs_val != rhs_val);
break;
case TokenType::GT:
result_val = (lhs_val > rhs_val);
break;
case TokenType::LT:
result_val = (lhs_val < rhs_val);
break;
case TokenType::GE:
result_val = (lhs_val >= rhs_val);
break;
case TokenType::LE:
result_val = (lhs_val <= rhs_val);
break;
}
values.push(result_val);
break;
}
default:
// Not implemented yet
values.push(false);
break;
}
}
// Ensure the final result is a boolean
if (values.empty()) return false;
if (std::holds_alternative<bool>(values.top())) {
return std::get<bool>(values.top());
}
// Last token was an option name, convert to existence check
return result.count(std::get<std::string>(values.top())) > 0;
}
std::string m_rule_str;
std::vector<Token> m_postfix;
std::string m_error_msg;
};
// Update Command to use EnhancedDependencyRule
void Command::add_dependency(std::string rule) {
m_dependencies.push_back(std::make_shared<EnhancedDependencyRule>(std::move(rule)));
}
} // namespace cxxoptspp

125
src/example_enhanced.cpp Normal file
View File

@ -0,0 +1,125 @@
#include <iostream>
#include <cxxoptspp.hpp>
using namespace cxxopts;
using namespace cxxoptspp;
int main(int argc, char** argv) {
try {
// Create root command
Command root_cmd("mytool", "A tool with enhanced features");
// Add root options
int verbose_level = 0;
root_cmd.add_options()
("h,help", "Print help message")
("v,verbose", "Increase verbosity", value(verbose_level))
("d,debug", "Enable debug mode", value<bool>());
// Add subcommands
auto module_cmd = root_cmd.add_subcommand("module", "Module subcommand");
auto subcmd = module_cmd->add_subcommand("subcmd", "Nested subcommand");
// Add module options
std::string module_name;
module_cmd->add_options()
("n,name", "Module name", value(module_name))
("m,mode", "Operation mode", value<std::string>());
// Add nested subcommand options with dependencies
std::string output_file;
bool overwrite = false;
subcmd->add_options()
("o,output", "Output file", value(output_file))
("w,overwrite", "Overwrite existing file", value(overwrite))
("s,size", "File size", value<int>())
("t,type", "File type", value<std::string>());
// Add dependency rules
// --output is only valid when --mode=file (inherited from module)
subcmd->add_dependency("(mode == file) ==> output");
// --verbose needs to be used with --debug
root_cmd.add_dependency("verbose ==> debug");
// Either --size or --type must be provided
subcmd->add_dependency("size || type");
// Example with custom type (IPAddress)
Command network_cmd = root_cmd.add_subcommand("network", "Network operations");
IPAddress server_ip;
std::vector<IPAddress> client_ips;
network_cmd->add_options()
("s,server", "Server IP address", value<IPAddress>())
("c,clients", "Client IP addresses (comma-separated)", value<std::vector<IPAddress>>());
// Parse command line
CommandParser parser(root_cmd);
ParseResult result = parser.parse(argc, argv);
// Handle help
if (result.count("help")) {
std::cout << root_cmd.options().help() << std::endl;
return 0;
}
// Display results
std::cout << "Root options:" << std::endl;
if (result.count("verbose")) {
std::cout << " Verbose level: " << result["verbose"].as<int>() << std::endl;
}
if (result.count("debug")) {
std::cout << " Debug mode: enabled" << std::endl;
}
const Command& current_cmd = parser.current_command();
std::cout << "\nCurrent command path: " << current_cmd.name() << std::endl;
// Show module options if applicable
if (result.count("name")) {
std::cout << "Module name: " << result["name"].as<std::string>() << std::endl;
}
if (result.count("mode")) {
std::cout << "Mode: " << result["mode"].as<std::string>() << std::endl;
}
if (result.count("output")) {
std::cout << "Output file: " << result["output"].as<std::string>() << std::endl;
}
if (result.count("overwrite")) {
std::cout << "Overwrite: " << (result["overwrite"].as<bool>() ? "yes" : "no") << std::endl;
}
// Example with IP address custom type
if (current_cmd.name() == "network") {
if (result.count("server")) {
IPAddress ip = result["server"].as<IPAddress>();
std::cout << "Server IP: " << ip.to_string() << std::endl;
}
if (result.count("clients")) {
auto ips = result["clients"].as<std::vector<IPAddress>>();
std::cout << "Client IPs: ";
for (size_t i = 0; i < ips.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << ips[i].to_string();
}
std::cout << std::endl;
}
}
// Example of reversible parsing
std::cout << "\nReversible parsing example:" << std::endl;
std::cout << "Original command: ";
for (int i = 0; i < argc; ++i) {
std::cout << argv[i] << " ";
}
std::cout << std::endl;
// TODO: Demonstrate serialization/deserialization
return 0;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}

234
src/reversible_parser.cpp Normal file
View File

@ -0,0 +1,234 @@
#include <cxxoptspp.hpp>
#include <nlohmann/json.hpp>
#include <sstream>
#include <iomanip>
#include <algorithm>
namespace cxxoptspp {
using json = nlohmann::json;
// ============================
// ParseNode Implementation
// ============================
ParseNode::ParseNode()
: type(Type::ROOT) {}
ParseNode::ParseNode(std::string name)
: type(Type::COMMAND), name(std::move(name)) {}
ParseNode::ParseNode(std::string name, std::string value)
: type(Type::OPTION), name(std::move(name)), value(std::move(value)) {}
json ParseNode::to_json() const {
json j;
j["type"] = static_cast<int>(type);
if (!name.empty()) {
j["name"] = name;
}
if (!value.empty()) {
j["value"] = value;
}
if (!children.empty()) {
json j_children = json::array();
for (const auto& child : children) {
j_children.push_back(child.to_json());
}
j["children"] = j_children;
}
if (!metadata.empty()) {
j["metadata"] = metadata;
}
return j;
}
ParseNode ParseNode::from_json(const json& j) {
ParseNode node;
int type_int = j.at("type").get<int>();
node.type = static_cast<ParseNode::Type>(type_int);
if (j.contains("name")) {
node.name = j.at("name").get<std::string>();
}
if (j.contains("value")) {
node.value = j.at("value").get<std::string>();
}
if (j.contains("children")) {
for (const auto& j_child : j.at("children")) {
node.children.push_back(from_json(j_child));
}
}
if (j.contains("metadata")) {
node.metadata = j.at("metadata").get<json>();
}
return node;
}
std::vector<std::string> ParseNode::to_command_line() const {
std::vector<std::string> args;
switch (type) {
case Type::ROOT:
for (const auto& child : children) {
auto child_args = child.to_command_line();
args.insert(args.end(), child_args.begin(), child_args.end());
}
break;
case Type::COMMAND:
args.push_back(name);
for (const auto& child : children) {
auto child_args = child.to_command_line();
args.insert(args.end(), child_args.begin(), child_args.end());
}
break;
case Type::OPTION:
if (value.empty()) {
// Boolean option, just use --name
args.push_back("--" + name);
} else {
// Value option, use --name=value or --name value
// Use the shorter form when possible
if (value.find(' ') == std::string::npos) {
args.push_back("--" + name + "=" + value);
} else {
args.push_back("--" + name);
args.push_back(value);
}
}
break;
}
return args;
}
// ============================
// ParseTree Implementation
// ============================
ParseTree::ParseTree()
: m_root(std::make_shared<ParseNode>()) {}
json ParseTree::to_json() const {
return m_root->to_json();
}
void ParseTree::from_json(const json& j) {
m_root = std::make_shared<ParseNode>(ParseNode::from_json(j));
}
std::string ParseTree::to_string() const {
return to_json().dump(2);
}
std::vector<std::string> ParseTree::to_command_line() const {
return m_root->to_command_line();
}
// ============================
// StateSerializer Implementation
// ============================
std::string StateSerializer::serialize(const CommandParser& parser, const cxxopts::ParseResult& result) {
json j;
// Serialize options with original input strings
for (const auto& [opt_name, opt_value] : parser.original_inputs()) {
j["original_inputs"][opt_name] = opt_value;
}
// Serialize command hierarchy
if (parser.current_command()) {
j["command_hierarchy"] = parser.command_hierarchy();
}
// Serialize option definitions (simplified)
// In real implementation, we'd need to track all option metadata
return j.dump(2);
}
std::string StateSerializer::serialize(const ParseTree& tree) {
return tree.to_string();
}
std::vector<std::string> StateSerializer::deserialize_to_command_line(const std::string& serialized) {
json j = json::parse(serialized);
ParseTree tree;
tree.from_json(j);
return tree.to_command_line();
}
// ============================
// CommandParser Implementation for reversible parsing
// ============================
void CommandParser::trace_option(const std::string& opt_name, const std::string& opt_value) {
m_original_inputs[opt_name] = opt_value;
}
std::unordered_map<std::string, std::string> CommandParser::original_inputs() const {
return m_original_inputs;
}
std::vector<std::string> CommandParser::command_hierarchy() const {
std::vector<std::string> hierarchy;
auto cmd = m_current_command;
while (cmd) {
hierarchy.push_back(cmd->name());
cmd = cmd->parent();
}
std::reverse(hierarchy.begin(), hierarchy.end());
return hierarchy;
}
// =============================================================
// Additional helper functions for JSON serialization/deserialization
// =============================================================
json Command::to_json() const {
json j;
j["name"] = m_name;
j["description"] = m_description;
if (m_parent) {
j["parent"] = m_parent->name();
}
// Serialize subcommands
json subcommands = json::array();
for (const auto& [name, cmd] : m_subcommands) {
subcommands.push_back(cmd->to_json());
}
j["subcommands"] = subcommands;
return j;
}
json Options::to_json() const {
json j;
// This would need to access the internal option details from cxxopts
// For demonstration purposes, we'll keep it simple
j["options"] = "... internal options ...";
return j;
}
} // namespace cxxoptspp

127
src/test_enhanced.cpp Normal file
View File

@ -0,0 +1,127 @@
#include <iostream>
#include <vector>
#include <cxxoptspp.hpp>
int main() {
// Test 1: Command hierarchy
std::cout << "Test 1: Command hierarchy" << std::endl;
try {
cxxoptspp::Command root_cmd("tool", "A tool with subcommands");
auto& module_cmd = root_cmd.add_subcommand("module", "A module command");
auto& subcmd_cmd = module_cmd.add_subcommand("subcmd", "A subcommand");
subcmd_cmd.add_option<int>("--opt", "An option");
cxxoptspp::CommandParser parser(root_cmd);
int argc = 4;
const char* argv[] = {"tool", "module", "subcmd", "--opt=42"};
auto result = parser.parse(argc, argv);
if (result.count("opt")) {
std::cout << "✓ Parsed --opt=" << result["opt"].as<int>() << std::endl;
} else {
std::cout << "✗ Failed to parse --opt" << std::endl;
}
} catch (const std::exception& e) {
std::cout << "✗ Exception: " << e.what() << std::endl;
}
// Test 2: Dependency rules
std::cout << std::endl << "Test 2: Dependency rules" << std::endl;
try {
cxxoptspp::Command cmd("test", "Dependency test");
cmd.add_option<bool>("--verbose", "Verbose output");
cmd.add_option<bool>("--debug", "Debug output");
// Add rule: --verbose requires --debug
cmd.add_dependency("--verbose ==> --debug");
cxxoptspp::CommandParser parser(cmd);
// This should fail because --verbose is used without --debug
int argc = 2;
const char* argv[] = {"test", "--verbose"};
try {
auto result = parser.parse(argc, argv);
std::cout << "✗ Dependency rule should have failed but passed" << std::endl;
} catch (const cxxopts::exceptions::parsing& e) {
std::cout << "✓ Dependency rule correctly caught violation: " << e.what() << std::endl;
}
} catch (const std::exception& e) {
std::cout << "✗ Exception: " << e.what() << std::endl;
}
// Test 3: Custom type IPAddress
std::cout << std::endl << "Test 3: Custom type IPAddress" << std::endl;
try {
cxxoptspp::Command cmd("test", "IP Address test");
cmd.add_option<cxxoptspp::IPAddress>("--ip", "An IP address");
cmd.add_option<std::vector<cxxoptspp::IPAddress>>("--ips", "Multiple IP addresses");
cxxoptspp::CommandParser parser(cmd);
int argc = 3;
const char* argv[] = {"test", "--ip=192.168.1.1", "--ips=10.0.0.1,10.0.0.2"};
auto result = parser.parse(argc, argv);
if (result.count("ip")) {
auto ip = result["ip"].as<cxxoptspp::IPAddress>();
std::cout << "✓ Parsed single IP: " << static_cast<int>(ip.octets[0]) << "."
<< static_cast<int>(ip.octets[1]) << "."
<< static_cast<int>(ip.octets[2]) << "."
<< static_cast<int>(ip.octets[3]) << std::endl;
} else {
std::cout << "✗ Failed to parse single IP" << std::endl;
}
if (result.count("ips")) {
auto ips = result["ips"].as<std::vector<cxxoptspp::IPAddress>>();
std::cout << "✓ Parsed multiple IPs (" << ips.size() << "):" << std::endl;
for (const auto& ip : ips) {
std::cout << " - " << static_cast<int>(ip.octets[0]) << "."
<< static_cast<int>(ip.octets[1]) << "."
<< static_cast<int>(ip.octets[2]) << "."
<< static_cast<int>(ip.octets[3]) << std::endl;
}
} else {
std::cout << "✗ Failed to parse multiple IPs" << std::endl;
}
} catch (const std::exception& e) {
std::cout << "✗ Exception: " << e.what() << std::endl;
}
// Test 4: Reversible parsing
std::cout << std::endl << "Test 4: Reversible parsing" << std::endl;
try {
cxxoptspp::Command cmd("test", "Reversible parsing test");
cmd.add_option<bool>("--verbose", "Verbose output");
cmd.add_option<int>("--count", "A count");
cmd.add_option<std::string>("--name", "A name");
cxxoptspp::CommandParser parser(cmd);
int argc = 5;
const char* argv[] = {"test", "--verbose", "--count=42", "--name=test_name"};
auto result = parser.parse(argc, argv);
// Get original inputs
auto original = parser.original_inputs();
std::cout << "✓ Original inputs:" << std::endl;
for (const auto& [opt, val] : original) {
std::cout << " - " << opt << "=" << val << std::endl;
}
} catch (const std::exception& e) {
std::cout << "✗ Exception: " << e.what() << std::endl;
}
std::cout << std::endl << "All tests completed" << std::endl;
return 0;
}

292
src/type_system.cpp Normal file
View File

@ -0,0 +1,292 @@
#include <cxxoptspp.hpp>
#include <sstream>
#include <stdexcept>
#include <regex>
namespace cxxoptspp {
// ============================
// Type System Implementation
// ============================
AbstractType::AbstractType(std::string name)
: m_name(std::move(name)) {}
AbstractType::~AbstractType() = default;
std::string AbstractType::name() const {
return m_name;
}
// ============================
// Built-in Type Implementations
// ============================
// StringType
template <>
std::string StringType::parse(const std::string& value) const {
return value;
}
template <>
std::string StringType::serialize(const std::string& value) const {
return value;
}
template <>
std::string StringType::to_string(const std::string& value) const {
return value;
}
// IntType
template <>
int IntType::parse(const std::string& value) const {
return std::stoi(value);
}
template <>
std::string IntType::serialize(const int& value) const {
return std::to_string(value);
}
template <>
std::string IntType::to_string(const int& value) const {
return std::to_string(value);
}
// BoolType
template <>
bool BoolType::parse(const std::string& value) const {
std::string lower = value;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return lower == "true" || lower == "1" || lower == "on" || lower == "yes";
}
template <>
std::string BoolType::serialize(const bool& value) const {
return value ? "true" : "false";
}
template <>
std::string BoolType::to_string(const bool& value) const {
return value ? "true" : "false";
}
// FloatType
template <>
float FloatType::parse(const std::string& value) const {
return std::stof(value);
}
template <>
std::string FloatType::serialize(const float& value) const {
return std::to_string(value);
}
template <>
std::string FloatType::to_string(const float& value) const {
return std::to_string(value);
}
// IPAddressType
IPAddressType::IPAddressType()
: AbstractType("ip_address") {}
std::any IPAddressType::parse(const std::string& value) const {
std::regex ip_regex(R"((\d+)\.(\d+)\.(\d+)\.(\d+))");
std::smatch match;
if (!std::regex_match(value, match, ip_regex)) {
throw std::invalid_argument("Invalid IP address: " + value);
}
IPAddress ip;
for (int i = 1; i <= 4; ++i) {
int octet = std::stoi(match[i]);
if (octet < 0 || octet > 255) {
throw std::invalid_argument("Invalid octet in IP address: " + value);
}
ip.octets[i-1] = octet;
}
return ip;
}
std::string IPAddressType::serialize(const std::any& value) const {
const IPAddress& ip = std::any_cast<const IPAddress&>(value);
std::ostringstream oss;
oss << (int)ip.octets[0] << "." << (int)ip.octets[1] << "." << (int)ip.octets[2] << "." << (int)ip.octets[3];
return oss.str();
}
std::string IPAddressType::to_string(const std::any& value) const {
return serialize(value);
}
// DateTimeRangeType
DateTimeRangeType::DateTimeRangeType()
: AbstractType("datetime_range") {}
std::any DateTimeRangeType::parse(const std::string& value) const {
// Simple implementation for demo
size_t dash_pos = value.find("-");
if (dash_pos == std::string::npos) {
throw std::invalid_argument("Invalid date range: " + value);
}
DateTimeRange range;
range.start = value.substr(0, dash_pos);
range.end = value.substr(dash_pos + 1);
// Validate dates (simplified)
// In real implementation, would use std::chrono or similar
return range;
}
std::string DateTimeRangeType::serialize(const std::any& value) const {
const DateTimeRange& range = std::any_cast<const DateTimeRange&>(value);
return range.start + "-" + range.end;
}
std::string DateTimeRangeType::to_string(const std::any& value) const {
return serialize(value);
}
// ============================
// Type Manager Implementation
// ============================
TypeManager& TypeManager::instance() {
static TypeManager manager;
return manager;
}
template <typename T>
void TypeManager::register_type(const std::shared_ptr<AbstractType>& type) {
m_types[typeid(T).hash_code()] = type;
m_type_names[type->name()] = type;
}
template <typename T>
std::shared_ptr<AbstractType> TypeManager::get_type() const {
auto it = m_types.find(typeid(T).hash_code());
if (it != m_types.end()) {
return it->second;
}
return nullptr;
}
std::shared_ptr<AbstractType> TypeManager::get_type_by_name(const std::string& name) const {
auto it = m_type_names.find(name);
if (it != m_type_names.end()) {
return it->second;
}
return nullptr;
}
// ============================
// Custom type parsers for cxxopts
// ============================
bool parse_value(const std::string& text, IPAddress& value) {
IPAddressType type;
try {
value = std::any_cast<IPAddress>(type.parse(text));
return true;
} catch (const std::exception&) {
return false;
}
}
bool parse_value(const std::string& text, std::vector<IPAddress>& value) {
IPAddressType type;
// Split by comma
size_t pos = 0;
std::string token;
std::string copy = text;
while ((pos = copy.find(',')) != std::string::npos) {
token = copy.substr(0, pos);
try {
value.push_back(std::any_cast<IPAddress>(type.parse(token)));
} catch (const std::exception&) {
return false;
}
copy.erase(0, pos + 1);
}
if (!copy.empty()) {
try {
value.push_back(std::any_cast<IPAddress>(type.parse(copy)));
} catch (const std::exception&) {
return false;
}
}
return true;
}
bool parse_value(const std::string& text, DateTimeRange& value) {
DateTimeRangeType type;
try {
value = std::any_cast<DateTimeRange>(type.parse(text));
return true;
} catch (const std::exception&) {
return false;
}
}
bool parse_value(const std::string& text, std::vector<DateTimeRange>& value) {
DateTimeRangeType type;
// Split by comma
size_t pos = 0;
std::string token;
std::string copy = text;
while ((pos = copy.find(',')) != std::string::npos) {
token = copy.substr(0, pos);
try {
value.push_back(std::any_cast<DateTimeRange>(type.parse(token)));
} catch (const std::exception&) {
return false;
}
copy.erase(0, pos + 1);
}
if (!copy.empty()) {
try {
value.push_back(std::any_cast<DateTimeRange>(type.parse(copy)));
} catch (const std::exception&) {
return false;
}
}
return true;
}
// ============================
// Collection Parser
// ============================
template <typename T>
bool CollectionParser<T>::parse(const std::string& value, char delimiter, std::vector<T>& result) {
result.clear();
std::stringstream ss(value);
std::string token;
while (std::getline(ss, token, delimiter)) {
T parsed_value;
if (!parse_value(token, parsed_value)) {
return false;
}
result.push_back(std::move(parsed_value));
}
return true;
}
} // namespace cxxoptspp