Skip to content

Custom Handlers

Custom handlers extend cargs' capabilities by allowing you to transform and validate input data according to your specific needs.

Overview

This guide covers advanced input processing with custom handlers:

  • Handler Function Signature - Creating custom processing functions
  • Memory Management - Allocating and freeing custom data structures
  • Data Transformation - Converting raw input to structured data
  • Custom Type Creation - Defining specialized data types
  • Integration with Validators - Combining handlers with validation

For basic option handling, see the Basic Options guide.

Understanding Handlers

While cargs provides standard handlers for common types (string, int, float, boolean), custom handlers enable more advanced processing:

  • Transforming inputs into complex data structures
  • Parsing specialized formats (host:port, coordinates, colors)
  • Converting between different representations
  • Validating with business rules during processing

Unlike validators that simply check values, custom handlers can transform input strings into sophisticated data structures.

Handler Function Signature

All handler functions must follow this signature:

int handler_function(cargs_t *cargs, cargs_option_t *option, char *arg);

Parameters: - cargs: The cargs context, used for error reporting - option: The option being processed, where to store the value - arg: The raw string value to process (NULL for boolean flags)

Return value: - CARGS_SUCCESS (0) on success - Any error code (non-zero) on failure

Creating a Custom Handler

Let's look at a complete example of a custom handler that parses an endpoint in the format "host:port":

typedef struct {
    char* host;
    int port;
} endpoint_t;

int endpoint_handler(cargs_t *cargs, cargs_option_t *option, char *arg)
{
    if (arg == NULL) {
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MISSING_VALUE, "Endpoint is required");
    }

    // Allocate the endpoint structure
    endpoint_t *endpoint = calloc(1, sizeof(endpoint_t));
    if (!endpoint) {
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MEMORY, "Memory allocation failed");
    }

    // Find the separator
    char *colon = strchr(arg, ':');
    if (!colon) {
        free(endpoint);
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_FORMAT, 
                          "Invalid endpoint format: %s (expected host:port)", arg);
    }

    // Extract host
    size_t host_len = colon - arg;
    endpoint->host = strndup(arg, host_len);
    if (!endpoint->host) {
        free(endpoint);
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MEMORY, "Memory allocation failed");
    }

    // Extract port
    long port = strtol(colon + 1, NULL, 10);
    if (port <= 0 || port > 65535) {
        free(endpoint->host);
        free(endpoint);
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE, 
                          "Invalid port: %ld (must be between 1 and 65535)", port);
    }

    endpoint->port = (int)port;

    // Store the endpoint structure
    option->value.as_ptr = endpoint;
    option->is_allocated = true;  // Important: mark as allocated for proper cleanup
    return CARGS_SUCCESS;
}

Free Handlers

When your handler allocates memory, you must provide a custom free handler:

int endpoint_free_handler(cargs_option_t *option)
{
    endpoint_t *endpoint = (endpoint_t*)option->value.as_ptr;
    if (endpoint) {
        free(endpoint->host);
        free(endpoint);
    }
    return CARGS_SUCCESS;
}

Using Custom Handlers

To use a custom handler, define an option with the OPTION_BASE macro:

CARGS_OPTIONS(
    options,
    HELP_OPTION(FLAGS(FLAG_EXIT)),

    // Option with custom handler
    OPTION_BASE('e', "endpoint", HELP("Server endpoint (host:port)"), 
                VALUE_TYPE_CUSTOM,
                HANDLER(endpoint_handler),
                FREE_HANDLER(endpoint_free_handler))
)

Creating Helper Macros

For reusability and cleaner code, you can create your own macro:

// Helper macro for endpoint options
#define OPTION_ENDPOINT(short_name, long_name, ...) \
    OPTION_BASE(short_name, long_name, VALUE_TYPE_CUSTOM, \
                HANDLER(endpoint_handler), \
                FREE_HANDLER(endpoint_free_handler), \
                ##__VA_ARGS__)

// Usage in option definitions
CARGS_OPTIONS(
    options,
    HELP_OPTION(FLAGS(FLAG_EXIT)),

    // Much cleaner with a dedicated macro
    OPTION_ENDPOINT('e', "endpoint", HELP("Server endpoint"))
)

Accessing Custom Values

To access values processed by custom handlers:

if (cargs_is_set(cargs, "endpoint")) {
    // Get and cast the value to the correct type
    endpoint_t *endpoint = (endpoint_t*)cargs_get(cargs, "endpoint").as_ptr;

    // Use the structured data
    printf("Host: %s\n", endpoint->host);
    printf("Port: %d\n", endpoint->port);
}

Advanced Techniques

Combining Handlers and Validators

You can use custom handlers with validators for complex processing:

// Custom handler for endpoint "host:port"
OPTION_BASE('e', "endpoint", HELP("Server endpoint (host:port)"), 
            VALUE_TYPE_CUSTOM,
            HANDLER(endpoint_handler),
            FREE_HANDLER(endpoint_free_handler),
            VALIDATOR(endpoint_validator, NULL))  // Add extra validation

Custom Types with Specialized Processing

For more complex types, you can implement specialized processing:

typedef struct {
    unsigned char r;
    unsigned char g;
    unsigned char b;
} rgb_color_t;

int color_handler(cargs_t *cargs, cargs_option_t *option, char *arg)
{
    // Allocate and initialize color structure
    rgb_color_t *color = calloc(1, sizeof(rgb_color_t));

    // Handle different formats:
    // - Hex: #RRGGBB or #RGB
    // - RGB: rgb(r,g,b)
    // - Named colors: red, green, blue, etc.

    // Store the color structure
    option->value.as_ptr = color;
    option->is_allocated = true;
    return CARGS_SUCCESS;
}
typedef struct {
    double lat;
    double lon;
} geo_coord_t;

int coordinate_handler(cargs_t *cargs, cargs_option_t *option, char *arg)
{
    // Allocate coordinate structure
    geo_coord_t *coord = calloc(1, sizeof(geo_coord_t));

    // Parse formats:
    // - Decimal: "lat,lon" 
    // - DMS: "40°45'30"N,74°0'23"W"

    // Store the coordinate structure
    option->value.as_ptr = coord;
    option->is_allocated = true;
    return CARGS_SUCCESS;
}

Error Reporting

Custom handlers should use the CARGS_REPORT_ERROR macro to report errors:

CARGS_REPORT_ERROR(cargs, error_code, format_string, ...);

This macro: 1. Formats an error message 2. Adds the error to the error stack 3. Returns the specified error code from your function

Common error codes: - CARGS_ERROR_MISSING_VALUE: Value was required but not provided - CARGS_ERROR_INVALID_FORMAT: Value has incorrect format - CARGS_ERROR_INVALID_VALUE: Value is semantically invalid - CARGS_ERROR_MEMORY: Memory allocation failed

Memory Management Key Points

When your handler allocates memory:

  1. Store the value in option->value (usually option->value.as_ptr for custom structures)
  2. Set option->is_allocated = true to indicate that memory needs to be freed
  3. Provide a custom free handler with FREE_HANDLER()
  4. Ensure proper cleanup in error cases

Best Practices

1. Robust Error Handling

Always implement comprehensive error handling:

// Allocate memory
endpoint_t *endpoint = calloc(1, sizeof(endpoint_t));
if (!endpoint) {
    CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MEMORY, "Memory allocation failed");
}

// Check for format errors
char *colon = strchr(arg, ':');
if (!colon) {
    free(endpoint);  // Clean up on error
    CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_FORMAT, 
                       "Invalid format: expected host:port");
}

2. Clear Error Messages

Provide informative error messages that help users:

CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_FORMAT,
                 "Invalid endpoint format: %s (expected host:port)", arg);
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_FORMAT, "Bad input");

3. Consistent Memory Management

Follow consistent memory management patterns:

int my_handler(cargs_t *cargs, cargs_option_t *option, char *arg)
{
    // 1. Validate input
    if (arg == NULL) { ... }

    // 2. Allocate main structure
    my_type_t *obj = calloc(1, sizeof(my_type_t));
    if (!obj) { ... }

    // 3. Process input and allocate additional memory as needed
    // ...

    // 4. On error, clean up all allocations and report error
    // ...

    // 5. On success, store result and mark as allocated
    option->value.as_ptr = obj;
    option->is_allocated = true;
    return CARGS_SUCCESS;
}

4. Type-Specific Helper Macros

Create helper macros for custom types:

#define OPTION_ENDPOINT(short_name, long_name, ...) \
    OPTION_BASE(short_name, long_name, VALUE_TYPE_CUSTOM, \
                HANDLER(endpoint_handler), \
                FREE_HANDLER(endpoint_free_handler), \
                ##__VA_ARGS__)

#define OPTION_COLOR(short_name, long_name, ...) \
    OPTION_BASE(short_name, long_name, VALUE_TYPE_CUSTOM, \
                HANDLER(color_handler), \
                FREE_HANDLER(color_free_handler), \
                ##__VA_ARGS__)

Complete Example

Here's a complete example implementing a custom endpoint handler:

#include "cargs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Structure for the endpoint
typedef struct {
    char* host;
    int port;
} endpoint_t;

// Custom handler for endpoint "host:port"
int endpoint_handler(cargs_t *cargs, cargs_option_t *option, char *arg)
{
    if (arg == NULL) {
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MISSING_VALUE, "Endpoint is required");
    }

    // Allocate the endpoint structure
    endpoint_t *endpoint = calloc(1, sizeof(endpoint_t));
    if (!endpoint) {
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MEMORY, "Memory allocation failed");
    }

    // Find the separator
    char *colon = strchr(arg, ':');
    if (!colon) {
        free(endpoint);
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_FORMAT, 
                          "Invalid endpoint format: %s (expected host:port)", arg);
    }

    // Extract host
    size_t host_len = colon - arg;
    endpoint->host = strndup(arg, host_len);
    if (!endpoint->host) {
        free(endpoint);
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_MEMORY, "Memory allocation failed");
    }

    // Extract port
    long port = strtol(colon + 1, NULL, 10);
    if (port <= 0 || port > 65535) {
        free(endpoint->host);
        free(endpoint);
        CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE, 
                          "Invalid port: %ld (must be between 1 and 65535)", port);
    }

    endpoint->port = (int)port;

    // Store the endpoint structure
    option->value.as_ptr = endpoint;
    option->is_allocated = true;
    return CARGS_SUCCESS;
}

// Free handler for endpoint structure
int endpoint_free_handler(cargs_option_t *option)
{
    endpoint_t *endpoint = (endpoint_t*)option->value.as_ptr;
    if (endpoint) {
        free(endpoint->host);
        free(endpoint);
    }
    return CARGS_SUCCESS;
}

// Helper macro for endpoint options
#define OPTION_ENDPOINT(short_name, long_name, ...) \
    OPTION_BASE(short_name, long_name, VALUE_TYPE_CUSTOM, \
                HANDLER(endpoint_handler), \
                FREE_HANDLER(endpoint_free_handler), \
                ##__VA_ARGS__)

// Option definitions
CARGS_OPTIONS(
    options,
    HELP_OPTION(FLAGS(FLAG_EXIT)),
    VERSION_OPTION(FLAGS(FLAG_EXIT)),

    // Server endpoint using the custom handler
    OPTION_ENDPOINT('s', "server", HELP("Server endpoint (host:port)")),

    // Database endpoint with default value
    OPTION_ENDPOINT('d', "database", HELP("Database endpoint (host:port)"), 
                   DEFAULT("localhost:5432"))
)

int main(int argc, char **argv)
{
    cargs_t cargs = cargs_init(options, "custom_handlers_example", "1.0.0");
    cargs.description = "Example of custom handlers";

    int status = cargs_parse(&cargs, argc, argv);
    if (status != CARGS_SUCCESS) {
        return status;
    }

    // Process server endpoint if provided
    if (cargs_is_set(cargs, "server")) {
        endpoint_t *server = cargs_get(cargs, "server").as_ptr;

        printf("Server information:\n");
        printf("  Host: %s\n", server->host);
        printf("  Port: %d\n", server->port);
        printf("\n");
    }

    // Process database endpoint if provided
    if (cargs_is_set(cargs, "database")) {
        endpoint_t *db = cargs_get(cargs, "database").as_ptr;

        printf("Database information:\n");
        printf("  Host: %s\n", db->host);
        printf("  Port: %d\n", db->port);
        printf("\n");
    }

    cargs_free(&cargs);
    return 0;
}