Custom Validators
This guide explains how to create and use custom validators with cargs to implement specialized validation logic for command-line options.
Overview
Validation is essential for ensuring that command-line inputs meet your application's requirements. While cargs provides built-in validators like RANGE() and REGEX(), custom validators allow you to implement application-specific validation logic.
In this guide, you'll learn about:
- The two types of validators: validators and pre-validators
- How to create and use custom validators
- Techniques for passing configuration data to validators
- Best practices for validator implementation
Understanding Validator Types
Cargs supports two distinct types of custom validation functions, each with a specific purpose:
Validators
Purpose: Check the processed value after type conversion
When to use: When validating based on the final data type (int, float, string, etc.)
Function signature:
Pre-validators
Purpose: Check the raw string before it's processed
When to use: When you need to: - Validate string format before parsing attempts - Perform complex string validation - Prevent type conversion errors
Function signature:
Creating Basic Validators
Let's start with simple examples of both validator types.
Example: Even Number Validator
This validator ensures that integer options have even values:
int even_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
// Not using custom data in this example
UNUSED(data);
if (option->value.as_int % 2 != 0) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be an even number, got %d", option->value.as_int);
}
return CARGS_SUCCESS;
}
Using the validator:
Example: String Length Pre-validator
This pre-validator checks if a string meets a minimum length requirement:
int string_length_pre_validator(cargs_t *cargs, const char *value, validator_data_t data)
{
// Get minimum length from the validator data
size_t min_length = *(size_t *)data.custom;
if (strlen(value) < min_length) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"String must be at least %zu characters long", min_length);
}
return CARGS_SUCCESS;
}
Using the pre-validator:
// Define the validation constraint
size_t min_length = 8;
OPTION_STRING('p', "password", HELP("Password"),
PRE_VALIDATOR(string_length_pre_validator, &min_length))
Passing Data to Validators
The validator_data_t parameter allows you to pass configuration data to your validators, making them more flexible and reusable.
Using Custom Data Structures
For complex validation rules, you can create a structure to hold multiple parameters:
typedef struct {
int min_value;
int max_value;
bool allow_odd;
} number_constraints_t;
int number_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
// Get constraints from validator data
number_constraints_t *constraints = (number_constraints_t *)data.custom;
// Range validation
if (option->value.as_int < constraints->min_value ||
option->value.as_int > constraints->max_value) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_RANGE,
"Value must be between %d and %d",
constraints->min_value, constraints->max_value);
}
// Even/odd validation
if (!constraints->allow_odd && (option->value.as_int % 2 != 0)) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be an even number");
}
return CARGS_SUCCESS;
}
Using the validator with custom data:
// Define constraints
static number_constraints_t constraints = {
.min_value = 10,
.max_value = 100,
.allow_odd = false
};
OPTION_INT('n', "number", HELP("A number with constraints"),
VALIDATOR(number_validator, &constraints))
Using Inline Compound Literals
For simple cases, you can use C99 compound literals to pass data inline:
OPTION_STRING('u', "username", HELP("Username"),
PRE_VALIDATOR(string_length_pre_validator, &((size_t){3})))
This creates an anonymous size_t variable with value 3 and passes its address to the validator.
Advanced Validation Techniques
Context-Aware Validation
Sometimes validators need to check values in relation to other options:
typedef struct {
const char *related_option;
} option_relation_t;
int greater_than_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
option_relation_t *relation = (option_relation_t *)data.custom;
cargs_value_t other_value = cargs_get(*cargs, relation->related_option);
if (option->value.as_int <= other_value.as_int) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be greater than '%s' (%d)",
relation->related_option, other_value.as_int);
}
return CARGS_SUCCESS;
}
Usage example:
static option_relation_t max_relation = { .related_option = "min" };
CARGS_OPTIONS(
options,
OPTION_INT('n', "min", HELP("Minimum value")),
OPTION_INT('x', "max", HELP("Maximum value"),
VALIDATOR(greater_than_validator, &max_relation))
)
Creating Helper Macros
For frequently used validation patterns, create helper macros:
// Helper macro for even number validation
#define EVEN_NUMBER() VALIDATOR(even_validator, NULL)
// Helper macro for minimum string length
#define MIN_LENGTH(min) \
PRE_VALIDATOR(string_length_pre_validator, &((size_t){min}))
// Helper macro for maximum string length
#define MAX_LENGTH(max) \
PRE_VALIDATOR(string_length_max_validator, &((size_t){max}))
// Combined length check
#define STRING_LENGTH(min, max) \
PRE_VALIDATOR(string_length_range_validator, &((length_range_t){min, max}))
Usage example:
CARGS_OPTIONS(
options,
OPTION_INT('n', "number", HELP("An even number"), EVEN_NUMBER()),
OPTION_STRING('p', "password", HELP("Password"), MIN_LENGTH(8)),
OPTION_STRING('u', "username", HELP("Username"), STRING_LENGTH(3, 20))
)
Combining Multiple Validators
Cargs allows you to apply multiple validators to a single option by using the numbered validator macros:
OPTION_INT('p', "port", HELP("Port number"),
VALIDATOR(is_even_validator, NULL), // First validator
VALIDATOR2(range_validator, &port_range), // Second validator
VALIDATOR3(port_validator, NULL)) // Third validator
Cargs has a limit of 4 validators per option, but you can modify the constant CARGS_MAX_VALIDATORS to increase this limit.
Note that built-in validators like RANGE(), LENGTH(), and COUNT() use the first validator slot. You can combine them with custom validators by using the second and subsequent slots:
OPTION_INT('p', "port", HELP("Port number"),
RANGE(1, 65535), // Uses first validator slot
VALIDATOR2(is_even_validator, NULL)) // Uses second validator slot
Error Reporting
Validators should use CARGS_REPORT_ERROR to provide clear error messages:
Common error codes:
| Error Code | Description | Typical Use |
|---|---|---|
CARGS_ERROR_INVALID_VALUE |
Value doesn't meet requirements | General validation failures |
CARGS_ERROR_INVALID_RANGE |
Value outside allowed range | Range validation |
CARGS_ERROR_INVALID_FORMAT |
Value has incorrect format | Format validation |
CARGS_ERROR_MEMORY |
Memory allocation failed | During validation processing |
Best Practices
1. Single Responsibility
Each validator should focus on one validation concern:
// Good: Two focused validators
int is_even_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data);
int in_range_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data);
// Use them together
OPTION_INT('n', "number", HELP("Number"),
VALIDATOR(is_even_validator, NULL),
VALIDATOR2(in_range_validator, &range))
2. Descriptive Error Messages
Provide clear, actionable error messages:
// Good: Clear and specific message
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Username must be 3-20 characters with only letters, numbers, and underscores");
// Bad: Vague message
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE, "Invalid input");
3. Parameter Safety
Always validate parameters and handle edge cases:
int string_length_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
// Check if value is NULL before using it
if (option->value.as_string == NULL) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE, "String cannot be NULL");
}
// Rest of the validation logic...
return CARGS_SUCCESS;
}
4. Memory Efficiency
Avoid unnecessary heap allocations in validators:
int efficient_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
// Use stack-based buffers for temporary operations
char buffer[256];
// Process value without heap allocations
return CARGS_SUCCESS;
}
5. Reusable Components
Design validators to be reusable across options:
// Generic validator for checking if a number is divisible by n
int divisible_by_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
int divisor = *(int *)data.custom;
if (option->value.as_int % divisor != 0) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be divisible by %d", divisor);
}
return CARGS_SUCCESS;
}
// Reuse with different configurations
OPTION_INT('n', "number", HELP("Number divisible by 2"),
VALIDATOR(divisible_by_validator, &((int){2})));
OPTION_INT('m', "multiple", HELP("Multiple of 5"),
VALIDATOR(divisible_by_validator, &((int){5})));
Complete Example
Here's a complete example demonstrating various custom validator techniques:
#include "cargs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// Custom validator for email addresses
int email_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
(void)data; // Unused parameter
const char* email = option->value.as_string;
if (!email) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Email address cannot be NULL");
}
// Check for @ character
const char* at = strchr(email, '@');
if (!at) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Email address must contain an '@' character");
}
// Check for domain
const char* dot = strchr(at, '.');
if (!dot) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Email domain must contain a '.' character");
}
return CARGS_SUCCESS;
}
// Custom validator for even numbers
int even_validator(cargs_t *cargs, cargs_option_t *option, validator_data_t data)
{
(void)data; // Unused parameter
int number = option->value.as_int;
if (number % 2 != 0) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be an even number");
}
return CARGS_SUCCESS;
}
// Custom pre-validator for string case requirements
int case_pre_validator(cargs_t *cargs, const char *value, validator_data_t data)
{
typedef enum { LOWERCASE, UPPERCASE, MIXED } case_requirement_t;
case_requirement_t req = *(case_requirement_t *)data.custom;
bool has_upper = false;
bool has_lower = false;
for (const char *p = value; *p; p++) {
if (isupper(*p)) has_upper = true;
if (islower(*p)) has_lower = true;
}
switch (req) {
case LOWERCASE:
if (has_upper) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be lowercase only");
}
break;
case UPPERCASE:
if (has_lower) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must be uppercase only");
}
break;
case MIXED:
if (!has_upper || !has_lower) {
CARGS_REPORT_ERROR(cargs, CARGS_ERROR_INVALID_VALUE,
"Value must contain both uppercase and lowercase letters");
}
break;
}
return CARGS_SUCCESS;
}
// Helper macros for common validations
#define EVEN_NUMBER() VALIDATOR(even_validator, NULL)
#define EMAIL_VALIDATOR() VALIDATOR(email_validator, NULL)
#define LOWERCASE_ONLY() PRE_VALIDATOR(case_pre_validator, &((int){LOWERCASE}))
#define UPPERCASE_ONLY() PRE_VALIDATOR(case_pre_validator, &((int){UPPERCASE}))
#define MIXED_CASE() PRE_VALIDATOR(case_pre_validator, &((int){MIXED}))
CARGS_OPTIONS(
options,
HELP_OPTION(FLAGS(FLAG_EXIT)),
VERSION_OPTION(FLAGS(FLAG_EXIT)),
// Built-in range validator
OPTION_INT('p', "port", HELP("Port number"),
DEFAULT(8080), RANGE(1, 65535)),
// Built-in choices validator
OPTION_STRING('l', "log-level", HELP("Log level"),
DEFAULT("info"),
CHOICES_STRING("debug", "info", "warning", "error")),
// Custom email validator
OPTION_STRING('e', "email", HELP("Email address"),
EMAIL_VALIDATOR()),
// Custom even number validator
OPTION_INT('n', "number", HELP("An even number"),
EVEN_NUMBER(),
DEFAULT(42)),
// String with case validation
OPTION_STRING('u', "username", HELP("Username (lowercase)"),
LOWERCASE_ONLY()),
// String with multiple validators
OPTION_STRING('p', "password", HELP("Password (mixed case)"),
MIXED_CASE())
)
int main(int argc, char **argv) {
cargs_t cargs = cargs_init(options, "validators_example", "1.0.0");
cargs.description = "Example of custom validators";
int status = cargs_parse(&cargs, argc, argv);
if (status != CARGS_SUCCESS) {
return status;
}
// Access parsed values
int port = cargs_get(cargs, "port").as_int;
const char* log_level = cargs_get(cargs, "log-level").as_string;
int number = cargs_get(cargs, "number").as_int;
const char* email = cargs_is_set(cargs, "email") ?
cargs_get(cargs, "email").as_string : "not set";
const char* username = cargs_is_set(cargs, "username") ?
cargs_get(cargs, "username").as_string : "not set";
const char* password = cargs_is_set(cargs, "password") ?
cargs_get(cargs, "password").as_string : "not set";
printf("Validated values:\n");
printf(" Port: %d (range: 1-65535)\n", port);
printf(" Log level: %s (choices: debug, info, warning, error)\n", log_level);
printf(" Even number: %d (must be even)\n", number);
printf(" Email: %s (must be valid email format)\n", email);
printf(" Username: %s (must be lowercase)\n", username);
printf(" Password: %s (must contain mixed case)\n", password);
cargs_free(&cargs);
return 0;
}
Related Documentation
- Validation Guide - Basic validation concepts
- Regular Expressions Guide - Validation with regex patterns