libenvpp

repository·main·Indexed 41 days ago

https://github.com/ph3at/libenvpp

A modern C++ library for type-safe, platform-independent environment variable management. It provides features for defining, parsing, and validating environment variables with support for built-in and custom types, range constraints, option variables, and typo detection via edit-distance.

Tokens
4.3K
Snippets
12
Records
16
Agent score
2%

What's inside libenvpp

  1. Overview of libenvpp

    main

    libenvpp is a modern, platform-independent, and type-safe C++ library designed for handling environment variables. It provides a high-level interface for common use cases while remaining extensible for complex requirements.

    Key features include:

    • Type-safe parsing of environment variables.
    • Support for required and optional variables (with default values).
    • Automatic parsing of built-in types.
    • Support for user-defined types via custom parsers and validators.
    • Specialized variable types like range and option variables.
    • Advanced detection capabilities, including typo detection (via edit-distance) and unused environment variable detection.
  2. Use a custom environment for testing

    main

    To avoid using the system environment, you can pass a custom std::unordered_map<std::string, std::string> as the first parameter to prefix::parse_and_validate.

    Precedence Rules:

    1. Global testing environment (highest)
    2. Custom environment
    3. System environment (only if the variable is NOT found in the global or custom environments; if a custom environment is provided, no fallback to system environment occurs if the key is missing from both the global and custom maps).
  3. Install libenvpp via CMake FetchContent

    main

    You can integrate libenvpp into your CMake project using FetchContent. Ensure you link against libenvpp::libenvpp.

    set(LIBENVPP_INSTALL ON CACHE BOOL "" FORCE) # If installation is desired.
    FetchContent_Declare(libenvpp
        GIT_REPOSITORY https://github.com/ph3at/libenvpp.git
        GIT_TAG v1.5.3
    )
    FetchContent_MakeAvailable(libenvpp)
    
    # ...
    
    target_link_libraries(myproject PUBLIC
        libenvpp::libenvpp
    )
  4. Use a global testing environment for unit tests

    main

    Libenvpp provides env::scoped_test_environment to inject key/value pairs into a global testing environment. This allows you to simulate environment variables without modifying the actual system environment.

    Key Behaviors:

    • Variables in the global testing environment take precedence over system and custom environment variables.
    • Once the scoped_test_environment instance goes out of scope, the variables are automatically removed.
    • Warning: Interacting with the global testing environment is not thread safe.
    const auto _ = env::scoped_test_environment({
        {"MYPROG_LOG_FILE_PATH", "/dev/null"},
        {"MYPROG_NUM_THREADS", "8"},
    });
  5. Basic usage with environment variable prefixes

    main

    The library is designed around the concept of a prefix. This allows you to group related environment variables (e.g., MYPROG_LOG_FILE_PATH, MYPROG_NUM_THREADS) under a single namespace.

    1. Create a prefix using env::prefix("PREFIX").
    2. Register variables using register_variable<T>(name) (optional) or register_required_variable<T>(name) (required).
    3. Call parse_and_validate() on the prefix object. This consumes the prefix object; do not use the prefix after this call.
    4. Check the result using .ok(). If successful, retrieve values using .get(id) for required variables or .get_or(id, default_value) for optional ones.
    #include <libenvpp/env.hpp>
    
    int main() {
        auto pre = env::prefix("MYPROG");
    
        // Register an optional variable
        const auto log_path_id = pre.register_variable<std::filesystem::path>("LOG_FILE_PATH");
        // Register a required variable
        const auto num_threads_id = pre.register_required_variable<unsigned int>("NUM_THREADS");
    
        // Parse and validate (consumes 'pre')
        const auto parsed_and_validated_pre = pre.parse_and_validate();
    
        if (parsed_and_validated_pre.ok()) {
            const auto log_path = parsed_and_validated_pre.get_or(log_path_id, "/default/log/path");
            const auto num_threads = parsed_and_validated_pre.get(num_threads_id);
        } else {
            std::cout << parsed_and_validated_pre.warning_message();
            std::cout << parsed_and_validated_pre.error_message();
        }
    }
  6. Configure libenvpp CMake options

    main

    When building libenvpp, you can tune its behavior using the following CMake options:

    OptionDefaultDescription
    LIBENVPP_EXAMPLESON (standalone) / OFF (dep)Enables building of example programs.
    LIBENVPP_TESTSON (standalone) / OFF (dep)Enables building of unit tests.
    LIBENVPP_CHECKSON (debug/test) / OFF (release)Custom assertions that throw exceptions instead of std::abort if LIBENVPP_TESTS is enabled.
    LIBENVPP_INSTALLOFFAdds an install target.
    LIBENVPP_USE_SYSTEM_DEPSOFFTries to use system dependencies before falling back to submodules.
  7. Register a custom parser and validator for a specific variable

    main

    If you need logic that applies only to a specific environment variable (rather than all variables of a certain type), you can pass a callable (function, lambda, or functor) directly to register_variable or register_required_variable.

    This callable acts as both the parser and the validator. If you use this method, the default_parser and default_validator for that type are not automatically invoked. You must manually delegate to them if needed. Errors should be reported by throwing env::parser_error or env::validation_error.

    std::filesystem::path path_parser_and_validator(const std::string_view str)
    {
        const auto log_path = std::filesystem::path(str);
        if (!std::filesystem::exists(log_path)) {
            if (!std::filesystem::create_directory(log_path)) {
                throw env::validation_error{"Unable to create log directory"};
            }
        } else if (!std::filesystem::is_directory(log_path)) {
            throw env::validation_error{"Log path is not a directory"};
        }
        return log_path;
    }
    
    // Usage
    auto pre = env::prefix("CUSTOM_PARSER_AND_VALIDATOR");
    const auto path_id = pre.register_required_variable<std::filesystem::path>("LOG_PATH", path_parser_and_validator);
  8. Bypass parsing with `set_for_testing`

    main

    If you want to bypass parsing and validation entirely during tests, use set_for_testing. This method directly sets the value for a registered variable using its ID. It ignores any values in the system, custom, or testing environments and does not perform any validation logic. When you later call get or get_or on the prefix, it returns this direct value.

    auto pre = env::prefix("MYPROG");
    
    const auto log_path_id = pre.register_variable<std::filesystem::path>("LOG_FILE_PATH");
    const auto num_threads_id = pre.register_required_variable<unsigned int>("NUM_THREADS");
    
    pre.set_for_testing(log_path_id, "/dev/null");
    pre.set_for_testing(num_threads_id, 8);
  9. Register deprecated environment variables

    main

    To notify users that an environment variable is being phased out, use register_deprecated. This allows you to attach a custom warning message that will be displayed if the variable is present in the environment.

    auto pre = env::prefix("APP");
    pre.register_deprecated("FEATURE", "The option 'APP_FEATURE' has been deprecated since version x.y. Please use SOMETHING instead.");
  10. Generate a help message for a prefix

    main

    After registering variables with a prefix, you can generate a formatted help message describing which environment variables are supported and whether they are required or optional. Call help_message() on the prefix object.

    auto pre = env::prefix("MYPROG");
    
    const auto log_path_id = pre.register_variable<std::filesystem::path>("LOG_FILE_PATH");
    const auto num_threads_id = pre.register_required_variable<unsigned int>("NUM_THREADS");
    
    std::cout << pre.help_message();
  11. How to implement a custom type parser

    main

    To support parsing of user-defined types, specialize the env::default_parser<T> template. The specialization must implement a call operator that accepts a std::string_view and returns the parsed type.

    To avoid reinventing the wheel, you should delegate the parsing of primitive members to the existing env::default_parser implementations.

    struct program_data {
        int number;
        float percent;
    };
    
    namespace env {
    template <>
    struct default_parser<program_data> {
        program_data operator()(const std::string_view str) const
        {
            const auto split_str = split(str, ',');
            if (split_str.size() != 2) {
                throw parser_error{"Expected 2 comma delimited values"};
            }
    
            auto parsed_data = program_data{};
            // Delegate to existing primitive parsers
            parsed_data.number = default_parser<int>{}(split_str[0]);
            parsed_data.percent = default_parser<float>{}(split_str[1]);
    
            return parsed_data;
        }
    };
    }
  12. Use range variables for numeric constraints

    main

    To ensure an environment variable falls within a specific inclusive range, use register_range<T>(name, min, max).

    • The min and max values are inclusive.
    • Range variables can be either optional or required.
    • They use the default_parser and default_validator for the type.
    • Note: You cannot provide a custom parser/validator function for a range variable; use a normal variable for that.
    • Note: When using get_or to provide a default value for an optional range variable, the range requirement is not enforced on the default value.
    auto pre = env::prefix("RANGE");
    const auto num_threads_id = pre.register_range<unsigned int>("NUM_THREADS", 1, std::thread::hardware_concurrency());
    const auto parsed_and_validated_pre = pre.parse_and_validate();
    
    if (parsed_and_validated_pre.ok()) {
        const auto num_threads = parsed_and_validated_pre.get_or(num_threads_id, std::thread::hardware_concurrency());
    }