Skip to content

Basic Usage

This comprehensive guide covers all fundamental concepts of using Argos to build command-line interfaces. After reading this, you'll understand how to create options, positional arguments, validation, constraints, and advanced features.

The Arguments Class

Every Argos application starts with a class that extends Arguments:

import onl.ycode.argos.Arguments

class MyApp : Arguments(
    appName = "myapp",
    appDescription = "My awesome CLI application",
    // Many other configuration options available
) {
    // Your options and configuration go here
}

Arguments Constructor Configuration

The Arguments class provides extensive configuration options:

class MyApp : Arguments(
    appName = "my-app",                          // Application name (default: class name)
    appDescription = "My awesome CLI tool",      // App description for help
    unknownOptionsAsPositionals = false,         // Treat unknown options as positional args
    defaultLongPrefix = "--",                    // Default prefix for long options
    clusterChar = '-',                           // Character for clustering short options (-abc)
    valueSeparators = setOf('=', ':'),           // Characters for option=value syntax
    didYouMean = true,                           // Enable "did you mean" suggestions
    didYouMeanMax = 2,                           // Max edit distance for suggestions
    aggregateErrors = true,                      // Show multiple errors at once
    maxAggregatedErrors = 20,                    // Max errors to show when aggregating
    terminal = null,                             // Custom terminal (default: auto-detect)
    useANSITerminal = true,                      // Use ANSI colors when supported
    argumentFilePrefix = '@',                    // Prefix for argument files (@file.txt)
    argumentSeparator = "|"                      // Separator in help text (--opt|-o)
) {
    // Options defined here...
}

Options vs Positional Arguments

Argos supports two main types of command-line arguments:

  • Options: Named parameters like --verbose or -f filename
  • Positional Arguments: Unnamed parameters specified by position like input.txt output.txt
# Options are named
myapp --verbose --format json --output result.txt

# Positional arguments are by position
myapp input.txt output.txt
#     ^^^^^^^^^  ^^^^^^^^^^
#     first      second
#     positional positional

Creating Options

Use the option() function to create command-line options:

Basic String Options

class MyApp : Arguments() {
    // Nullable string option (default)
    val name by option("--name", "-n").help("Your name")

    // Non-nullable with default value
    val format by option("--format", "-f").default("json")
        .help("Output format")

    // Multiple switches for the same option
    val help by option("--help", "-h", "-?").bool().default(false)
}

Type Conversions

Argos provides built-in type conversions:

class MyApp : Arguments() {
    // Numeric types
    val count by option("--count").int()           // Int?
    val rate by option("--rate").float()           // Float?
    val size by option("--size").long()            // Long?
    val precision by option("--precision").double() // Double?

    // With defaults (non-nullable)
    val port by option("--port").int().default(8080)      // Int
    val timeout by option("--timeout").long().default(30L) // Long

    // Boolean options
    val verbose by option("--verbose", "-v").bool()       // Boolean?
    val debug by option("--debug").bool().default(false)  // Boolean

    // Enum types
    val level by option("--level").enum<LogLevel>()       // LogLevel?
    val mode by option("--mode").enum<ProcessMode>().default(ProcessMode.AUTO)

    // Restricted string values
    val format by option("--format").oneOf("json", "xml", "yaml")
}

enum class LogLevel { DEBUG, INFO, WARN, ERROR }
enum class ProcessMode { AUTO, MANUAL, BATCH }

Collection Types

class MyApp : Arguments() {
    // Lists (preserves order, allows duplicates)
    val files by option("--file", "-f").list()           // List<String>
    val ports by option("--port").int().list()           // List<Int>

    // Sets (no duplicates, preserves insertion order)
    val categories by option("--category").set()         // Set<String>
    val levels by option("--level").int().set()          // Set<Int>

    // Count options (counts occurrences)
    val verbosity by option("-v", "--verbose").count()   // Int (0, 1, 2, ...)
}

Boolean Options with Negation

class MyApp : Arguments() {
    // Standard boolean
    val cache by option("--cache").bool().default(true)

    // Negatable boolean (auto-generates --no-cache)
    val colors by option("--colors").bool().negatable().default(true)

    // Custom negation prefix
    val compress by option("--compress").bool().negatable("disable-").default(true)
    // Generates: --compress and --disable-compress
}

Positional Arguments

Positional arguments are specified by position rather than by name:

class MyApp : Arguments() {
    // Single positional (nullable)
    val inputFile by positional()                        // String?

    // Required positional (non-nullable)
    val outputFile by positional().required()            // String

    // Positional with default
    val configFile by positional().default("config.json") // String

    // List of positional arguments (collects remaining args - must be last)
    val additionalFiles by positional().list()           // List<String>
}

// Alternative: Set of positional arguments (also must be last)
class MyAppWithSet : Arguments() {
    val inputFile by positional()                        // String?
    val outputFile by positional().required()            // String
    val configFile by positional().default("config.json") // String

    // Set of positional arguments (must be last)
    val sources by positional().set()                    // Set<String>
}

Environment Variables

Options can read from environment variables as fallback:

class MyApp : Arguments() {
    val apiKey by option("--api-key")
        .fromEnv("API_KEY")
        .help("API key for authentication")

    val dbUrl by option("--db-url")
        .fromEnv("DATABASE_URL")
        .default("sqlite:memory:")
}

Validation

Argos provides comprehensive validation capabilities:

Single Value Validation

class MyApp : Arguments() {
    val email by option("--email")
        .validate("Must be valid email") { it?.contains("@") == true }

    val port by option("--port").int()
        .validate("Port must be in valid range") { it in 1..65535 }

    // Multiple validations
    val username by option("--username")
        .validate(
            "Username must be at least 3 characters" to { it?.length?.let { len -> len >= 3 } == true },
            "Username must contain only alphanumeric chars" to { it?.all { c -> c.isLetterOrDigit() } == true }
        )
}

Collection Validation

class MyApp : Arguments() {
    val ports by option("--port").int().list()
        .validate("Each port must be in valid range") { it in 1..65535 }          // Per-element
        .validateCollection("Must have 1-5 ports") { it.size in 1..5 }           // Entire collection

    val files by option("--file").list()
        .validate("File must exist") { File(it).exists() }                       // Per-element
        .validateCollection("Must specify at least one file") { it.isNotEmpty() } // Collection
}

Constraints and Requirements

Basic Requirements

class MyApp : Arguments() {
    // Mark scalar option as required
    val input by option("--input").required()

    // Require multiple elements in collections
    val tags by option("--tag").list().atLeast(2)

    // For count options, require minimum occurrences
    val verbosity by option("-v").count().atLeast(1)
}

Conditional Requirements

class MyApp : Arguments() {
    val input by option("--input")
    val output by option("--output")
    val config by option("--config")
    val mode by option("--mode").oneOf("fast", "slow")

    // Require backup when output is specified
    val backup by option("--backup").requireIfAnyPresent(::output)

    // Require verify when both input and output are present
    val verify by option("--verify").requireIfAllPresent(::input, ::output)

    // Require threads when mode is "fast"
    val threads by option("--threads").int().requireIfValue(::mode) { it == "fast" }
}

Group Constraints

The *With constraint methods (exactlyOneWith, atMostOneWith, atLeastOneWith) automatically include the receiver option in the constraint group. For example, exactlyOneWith(::input) creates a group containing both the receiver option and input.

class MyApp : Arguments() {
    val input by option("--input")
    val output by option("--output")
    val format by option("--format")
    val raw by option("--raw").bool()
    val json by option("--json").bool()
    val verbose by option("--verbose").bool()
    val quiet by option("--quiet").bool()

    // Exactly one must be provided (group: {source, input})
    val source by option("--source").exactlyOneWith(::input)

    // At most one can be provided (group: {formatOpt, raw, json})
    val formatOpt by option("--format-opt").atMostOneWith(::raw, ::json)

    // At least one must be provided (group: {dest, output})
    val dest by option("--dest").atLeastOneWith(::output)

    // Options that conflict with each other
    val verboseOpt by option("--verbose-opt").conflictsWith(::quiet)
}

Custom Transformations

Use the map() function for custom type transformations:

import java.io.File
import java.net.URL

class MyApp : Arguments() {
    // Transform to File
    val configFile by option("--config")
        .map("a valid file path") { path ->
            path?.let { File(it).takeIf { f -> f.exists() } }
        }

    // Transform to URL
    val endpoint by option("--endpoint")
        .map("a valid URL") { url ->
            url?.let {
                try { URL(it) } catch (e: Exception) { null }
            }
        }

    // Custom validation in transformation
    val percentage by option("--percentage")
        .map("a percentage (0-100)") { value ->
            value?.toIntOrNull()?.takeIf { it in 0..100 }
        }
}

Advanced Option Features

Option Value Requirements

class MyApp : Arguments() {
    // Control whether option requires a value
    val debugLevel by option("--debug-level")
        .int()
        .requiresValue(false)              // Can work as flag or with value

    val output by option("--output")
        .requiresValue(true)               // Always requires value
}

Password Options

class MyApp : Arguments() {
    val password by option("--password")
        .password(
            prompt = "Enter password: ",
            confirmPrompt = "Confirm password: ",
            requireConfirmation = true,
            mismatchPrompt = "Passwords don't match. Try again.",
            maxRetries = 3
        )
        .hidden()                          // Don't show in help
}

Hidden Options

class MyApp : Arguments() {
    val secretKey by option("--secret")
        .fromEnv("SECRET_KEY")
        .hidden()                          // Don't show in help output
}

Callbacks

class MyApp : Arguments() {
    val verbose by option("--verbose").bool().default(false)
        .onValue { isVerbose ->
            if (isVerbose) {
                println("Verbose mode enabled")
            }
        }

    val logLevel by option("--log-level").enum<LogLevel>().default(LogLevel.INFO)
        .onValue { level ->
            configureLogging(level)
        }
}

Built-in Helper Methods

Help and Version

class MyApp : Arguments() {
    // Auto-generates help option with default switches (-h, --help)
    val help by help()

    // Custom help switches
    val helpCustom by help("-?", "--help", "--usage")

    // Version option
    val version by version("1.0.0")

    // Custom version switches
    val versionCustom by version("1.2.3", "--version", "-V")
}

Parsing Methods

Parse with Callbacks

fun main(args: Array<String>) {
    val app = MyApp()

    app.parse(args) ?: return

    println("Parsing successful!")
    // Process the application logic
    processApp(app)
}

Simple Parse Method

fun main(args: Array<String>) {
    val app = MyApp()

    app.parse(args) ?: return

    println("Arguments parsed successfully")
    processApp(app)
}

Value Source Tracking

Track how each option's value was obtained:

fun main(args: Array<String>) {
    val app = MyApp()
    app.parse(args) ?: return

    // Check value sources
    val apiKeySource = app.valueSourceOf(app::apiKey)
    when (apiKeySource) {
        ValueSource.USER -> println("API key provided by user")
        ValueSource.ENVIRONMENT -> println("API key from environment")
        ValueSource.DEFAULT -> println("API key using default")
        ValueSource.MISSING -> println("API key not provided")
    }
}

Complete Example

Here's a comprehensive example showcasing many features:

import onl.ycode.argos.Arguments
import onl.ycode.argos.parse
import java.io.File

enum class LogLevel { DEBUG, INFO, WARN, ERROR }
enum class OutputFormat { JSON, XML, YAML, CSV }

class FileProcessor : Arguments(
    appName = "fileprocessor",
    appDescription = "Advanced file processing tool with multiple options",
    aggregateErrors = true,
    didYouMean = true
) {
    // Basic options
    val verbose by option("--verbose", "-v").bool().negatable().default(false)
        .help("Enable verbose output (use --no-verbose to disable)")

    val logLevel by option("--log-level", "-l").enum<LogLevel>().default(LogLevel.INFO)
        .help("Set logging level")

    // File operations
    val inputFiles by option("--input", "-i").list()
        .validate("File must exist") { File(it).exists() }
        .validateCollection("Must specify at least one input file") { it.isNotEmpty() }
        .help("Input files to process")

    val outputDir by option("--output-dir", "-o")
        .map("existing directory") { path ->
            path?.let { File(it).takeIf { f -> f.isDirectory } }
        }
        .help("Output directory")

    // Format and processing options
    val format by option("--format", "-f").enum<OutputFormat>().default(OutputFormat.JSON)
        .help("Output format")

    val compress by option("--compress", "-c").bool().default(false)
        .help("Compress output files")

    val threads by option("--threads", "-t").int().default(1)
        .validate("Thread count must be positive") { it > 0 }
        .help("Number of processing threads")

    // Advanced options
    val configFile by option("--config").fromEnv("PROCESSOR_CONFIG")
        .map("readable config file") { path ->
            path?.let { File(it).takeIf { f -> f.canRead() } }
        }
        .help("Configuration file path")

    val apiKey by option("--api-key").fromEnv("API_KEY").hidden()
        .help("API key for external services")

    // Conditional requirements
    val backup by option("--backup").requireIfAnyPresent(::outputDir)
        .help("Create backup before processing")

    val verify by option("--verify").requireIfValue(::format) { it == OutputFormat.XML }
        .help("Verify XML output (required for XML format)")

    // Conflicting options
    val quietMode by option("--quiet", "-q").bool().conflictsWith(::verbose)
        .help("Suppress all output")

    // Built-in options
    val help by help()
    val version by version("2.1.0")
}

fun main(args: Array<String>) {
    val processor = FileProcessor()

    processor.parse(args) ?: return

    println("File processor starting...")
    println("Input files: ${processor.inputFiles}")
    println("Output format: ${processor.format}")
    println("Verbose mode: ${processor.verbose}")

    // Check value sources
    val configSource = processor.valueSourceOf(processor::configFile)
    if (configSource == ValueSource.ENVIRONMENT) {
        println("Using config from environment variable")
    }

    // Process files here...
    processFiles(processor)
}

fun processFiles(processor: FileProcessor) {
    // Implementation here...
    println("Processing ${processor.inputFiles.size} files...")
}

This example demonstrates:

  • Multiple option types and conversions
  • Validation at both element and collection levels
  • Conditional requirements and constraints
  • Environment variable fallbacks
  • Custom transformations with map()
  • Hidden options for sensitive data
  • Value source tracking
  • Comprehensive error handling
  • Built-in help and version support