Validation¶
Argos provides comprehensive validation capabilities to ensure command-line arguments meet your application's requirements. Validation happens after type conversion and provides clear error messages to users.
Basic Validation¶
Validate single values using predicates:
class MyApp : Arguments() {
val email by option("--email")
.validate("Must be a valid email address") { it?.contains("@") == true }
val port by option("--port").int()
.validate("Port must be in valid range") { it in 1..65535 }
val percentage by option("--load").double()
.validate("Percentage must be between 0 and 100") { it in 0.0..100.0 }
}
Usage Examples¶
# Valid inputs
myapp --email user@example.com --port 8080 --load 75.5
# Invalid inputs generate clear errors
myapp --email invalid-email
# Error: Invalid value for --email: 'invalid-email' - Must be a valid email address
myapp --port 99999
# Error: Invalid value for --port: '99999' - Port must be in valid range
Multiple Validations¶
Apply multiple validation rules to the same option:
class MyApp : Arguments() {
// Method 1: Multiple validate() calls
val username by option("--username")
.validate("Username must be at least 3 characters") { it?.length?.let { len -> len >= 3 } == true }
.validate("Username must contain only alphanumeric characters") {
it?.all { c -> c.isLetterOrDigit() } == true
}
// Method 2: Multiple validations at once
val password by option("--password")
.validate(
"Password must be at least 8 characters" to { it?.length?.let { len -> len >= 8 } == true },
"Password must contain uppercase letter" to { it?.any { c -> c.isUpperCase() } == true },
"Password must contain lowercase letter" to { it?.any { c -> c.isLowerCase() } == true },
"Password must contain digit" to { it?.any { c -> c.isDigit() } == true }
)
}
Error Messages¶
When validation fails, users see specific error messages:
myapp --username ab
# Error: Invalid value for --username: 'ab' - Username must be at least 3 characters
myapp --password simple
# Error: Invalid value for --password: 'simple' - Password must contain uppercase letter
# Error: Invalid value for --password: 'simple' - Password must contain digit
Collection Validation¶
Per-Element Validation¶
Validate each element in a collection:
class MyApp : Arguments() {
val ports by option("--port").int().list()
.validate("Each port must be in valid range") { it in 1..65535 }
val emails by option("--email").list()
.validate("Each email must be valid") { it.contains("@") }
val files by option("--file").list()
.validate("File must exist") { File(it).exists() }
.validate("File must be readable") { File(it).canRead() }
}
Collection-Wide Validation¶
Validate the entire collection:
class MyApp : Arguments() {
val servers by option("--server").list()
.validate("Server name must be valid") { it.isNotBlank() } // Per-element
.validateCollection("Must have 1-5 servers") { it.size in 1..5 } // Collection-wide
val categories by option("--category").set()
.validateCollection("Must include 'core' category") { "core" in it }
.validateCollection("Cannot exceed 10 categories") { it.size <= 10 }
val priorities by option("--priority").int().list()
.validate("Priority must be 1-10") { it in 1..10 } // Per-element
.validateCollection("Priorities must be unique") { it.toSet().size == it.size } // No duplicates
}
Advanced Validation Patterns¶
File System Validation¶
import java.io.File
class MyApp : Arguments() {
val inputFile by option("--input")
.validate("Input file must exist") { it?.let { File(it).exists() } == true }
.validate("Input file must be readable") { it?.let { File(it).canRead() } == true }
.validate("Input file must not be empty") { it?.let { File(it).length() > 0 } == true }
val outputDir by option("--output-dir")
.validate("Output directory must exist") { it?.let { File(it).isDirectory } == true }
.validate("Output directory must be writable") { it?.let { File(it).canWrite() } == true }
val configFile by option("--config")
.validate("Config file must be JSON or YAML") { path ->
path?.let {
it.endsWith(".json") || it.endsWith(".yml") || it.endsWith(".yaml")
} == true
}
}
Network Validation¶
import java.net.URL
class MyApp : Arguments() {
val endpoint by option("--endpoint")
.validate("Must be a valid URL") { url ->
url?.let {
try { URL(it); true } catch (e: Exception) { false }
} == true
}
.validate("Must use HTTPS") { it?.startsWith("https://") == true }
val hostPort by option("--host-port")
.validate("Must be in host:port format") { it?.contains(":") == true }
.validate("Port must be valid") { hostPort ->
hostPort?.let {
val parts = it.split(":")
if (parts.size == 2) {
val port = parts[1].toIntOrNull()
port != null && port in 1..65535
} else false
} == true
}
}
Business Logic Validation¶
import java.time.LocalDate
import java.time.format.DateTimeFormatter
class MyApp : Arguments() {
val startDate by option("--start-date")
.map("date in YYYY-MM-DD format") {
it?.let {
try { LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) }
catch (e: Exception) { null }
}
}
.validate("Start date cannot be in the past") {
it?.isAfter(LocalDate.now().minusDays(1)) == true
}
val endDate by option("--end-date")
.map("date in YYYY-MM-DD format") {
it?.let {
try { LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) }
catch (e: Exception) { null }
}
}
val budget by option("--budget").double()
.validate("Budget must be positive") { it > 0 }
.validate("Budget cannot exceed $1,000,000") { it <= 1_000_000.0 }
val teamSize by option("--team-size").int()
.validate("Team size must be reasonable") { it in 1..50 }
}
Cross-Field Validation¶
For validation that depends on multiple fields, use custom validation in your application logic:
class ProjectApp : Arguments() {
val startDate by option("--start-date")
.map("date in YYYY-MM-DD format") {
it?.let { LocalDate.parse(it) }
}
val endDate by option("--end-date")
.map("date in YYYY-MM-DD format") {
it?.let { LocalDate.parse(it) }
}
val budget by option("--budget").double()
val teamSize by option("--team-size").int()
// Custom validation method
fun validateCrossFields(): List<String> {
val errors = mutableListOf<String>()
// Date validation
if (startDate != null && endDate != null && startDate!! >= endDate!!) {
errors.add("End date must be after start date")
}
// Budget per person validation
if (budget != null && teamSize != null && teamSize!! > 0) {
val budgetPerPerson = budget!! / teamSize!!
if (budgetPerPerson < 1000) {
errors.add("Budget per team member is too low (minimum $1,000)")
}
}
return errors
}
}
fun main(args: Array<String>) {
val app = ProjectApp().parse(args) ?: return
// Perform cross-field validation
val crossFieldErrors = app.validateCrossFields()
if (crossFieldErrors.isNotEmpty()) {
crossFieldErrors.forEach { println("Error: $it") }
return
}
// Continue with application logic...
}
Validation Error Messages¶
Template Variables¶
Validation error messages support template variables:
class MyApp : Arguments() {
val port by option("--port", "-p").int()
.validate("Invalid value for @name: @value - Port must be 1-65535") {
it in 1..65535
}
val config by option("--config").list()
.validate("@switches option: '@value' is not a valid config file") {
it.endsWith(".json") || it.endsWith(".yaml")
}
}
Available template variables: - @name
: Property name - @value
: The invalid value (formatted appropriately) - @switches
: Option switches (e.g., "--port|-p")
Custom Error Messages¶
class MyApp : Arguments() {
val apiKey by option("--api-key")
.validate("API key must be exactly 32 characters long") {
it?.length == 32
}
.validate("API key must contain only hexadecimal characters") {
it?.all { c -> c in "0123456789abcdefABCDEF" } == true
}
val retryCount by option("--retries").int()
.validate("Retry count must be reasonable (0-10)") {
it in 0..10
}
}
Complex Validation Examples¶
Build Configuration Validator¶
enum class BuildType { DEBUG, RELEASE, PROFILE }
class BuildConfig : Arguments() {
val buildType by option("--type").enum<BuildType>().default(BuildType.DEBUG)
val optimizationLevel by option("--optimization", "-O").int()
.validate("Optimization level must be 0-3") { it in 0..3 }
val sourceFiles by option("--source").list()
.validate("Source file must exist") { File(it).exists() }
.validate("Source file must be .cpp or .c file") {
it.endsWith(".cpp") || it.endsWith(".c") || it.endsWith(".cc")
}
.validateCollection("At least one source file required") { it.isNotEmpty() }
val includePaths by option("--include", "-I").list()
.validate("Include path must exist") { File(it).isDirectory }
.validate("Include path must be readable") { File(it).canRead() }
val defines by option("--define", "-D").list()
.validate("Define must be in KEY=VALUE format") {
it.contains("=") && it.split("=").size == 2
}
.validate("Define key must be valid C identifier") {
val key = it.split("=")[0]
key.isNotEmpty() && key[0].isLetter() && key.all { c -> c.isLetterOrDigit() || c == '_' }
}
val warningLevel by option("--warnings").int().default(2)
.validate("Warning level must be 0-4") { it in 0..4 }
val maxErrors by option("--max-errors").int().default(50)
.validate("Max errors must be positive") { it > 0 }
.validate("Max errors should be reasonable") { it <= 1000 }
}
Database Connection Validator¶
class DatabaseConfig : Arguments() {
val host by option("--host").default("localhost")
.validate("Host must not be empty") { it.isNotBlank() }
val port by option("--port").int().default(5432)
.validate("Port must be valid") { it in 1..65535 }
val database by option("--database").required()
.validate("Database name must be valid") {
it.isNotBlank() && it.all { c -> c.isLetterOrDigit() || c in "_-" }
}
val username by option("--username").required()
.validate("Username must not be empty") { it.isNotBlank() }
val password by option("--password").password()
.validate("Password must be at least 8 characters") { it.length >= 8 }
val connectionTimeout by option("--timeout").int().default(30)
.validate("Timeout must be positive") { it > 0 }
.validate("Timeout should be reasonable") { it <= 300 }
val sslMode by option("--ssl-mode").oneOf("disable", "require", "verify-ca", "verify-full")
.default("require")
val poolSize by option("--pool-size").int().default(10)
.validate("Pool size must be positive") { it > 0 }
.validate("Pool size should be reasonable") { it <= 100 }
}
Best Practices¶
1. Provide Clear, Actionable Messages¶
// Good: Specific, actionable error messages
val port by option("--port").int()
.validate("Port must be between 1 and 65535") { it in 1..65535 }
val email by option("--email")
.validate("Email must contain @ symbol") { it?.contains("@") == true }
// Avoid: Vague or confusing messages
val port by option("--port").int()
.validate("Invalid port") { it in 1..65535 }
2. Validate Early and Often¶
// Good: Validate inputs as soon as they're parsed
val configFile by option("--config")
.validate("Config file must exist") { File(it ?: "").exists() }
.validate("Config file must be readable") { File(it ?: "").canRead() }
// Better: Let the application fail later with unclear errors
3. Use Appropriate Validation Level¶
// Good: Essential validations in CLI parsing
val port by option("--port").int()
.validate("Port must be valid") { it in 1..65535 }
// Good: Business logic validation in application
fun validateProjectConfiguration(config: ProjectConfig) {
// Complex business rules here
}
// Avoid: All validation in CLI layer (makes it complex)
// Avoid: No validation in CLI layer (poor user experience)
4. Consider User Experience¶
// Good: Help users understand what's expected
val retries by option("--retries").int()
.validate("Retries must be 0-10 (0 = no retries, 10 = maximum)") {
it in 0..10
}
// Good: Accept common variations
val boolValue by option("--enabled")
.validate("Must be true/false, yes/no, on/off, or 1/0") { value ->
value?.lowercase() in listOf("true", "false", "yes", "no", "on", "off", "1", "0")
}
Validation ensures your CLI application receives valid inputs and provides clear feedback when users make mistakes. Design validation rules that match your application's requirements while providing helpful guidance to users.