Writing CLI Applications
This guide covers building command-line interfaces with the (sigil args) library. You'll learn to define options, parse arguments, create subcommands, and generate help text.
Overview
The (sigil args) library provides a declarative approach to CLI parsing. You define your interface with structs, then parse and execute with a single call.
(import (sigil args))
(define cmd
(command
name: "greet"
description: "Greet someone"
options: (list
(option name: 'name short: #\n long: "name"
value: "NAME" description: "Name to greet"))))
(define (main opts args)
(let ((name (alist-get 'name opts "World")))
(display (string-append "Hello, " name "!\n"))))
(run-command cmd main (command-line-args))Defining Options
Options are defined with the option struct. Each option needs at minimum a name (symbol) and either a short (character) or long (string) form.
Flags
Flags are boolean options that don't take a value:
(option name: 'verbose short: #\v long: "verbose"
description: "Enable verbose output")Usage: -v, --verbose
Result: (verbose . #t) in the parsed options.
Options with Values
Add a value: field to make an option accept a value:
(option name: 'output short: #\o long: "output"
value: "FILE" description: "Output file path")Usage: -o file.txt, -ofile.txt, --output file.txt, --output=file.txt
Result: (output . "file.txt") in the parsed options.
Required Options
Mark options as required to enforce they must be provided:
(option name: 'token long: "token"
value: "TOKEN" required: #t
description: "API token (required)")Missing required options produce an error in parse-result-errors.
Default Values
Provide fallback values for optional options:
(option name: 'port short: #\p long: "port"
value: "PORT" default: "8080"
description: "Server port (default: 8080)")Value Parsing
Transform string values with a parser function:
(option name: 'count short: #\n long: "count"
value: "N" parse: string->number
description: "Number of iterations")The parser receives the string value and returns the transformed value.
Advanced Option Features
Negatable Flags
Flags that support --no-X form to explicitly disable:
(option name: 'color long: "color" negatable: #t
description: "Enable colored output")Usage:
--colorsets(color . #t)--no-colorsets(color . #f)
Help text shows: --[no-]color
Multi-Value Options
Options that can be repeated, accumulating values into a list:
(option name: 'include short: #\I long: "include"
value: "PATH" multi: #t
description: "Include paths (can be repeated)")Usage: -I src -I lib --include vendor
Result: (include . ("src" "lib" "vendor"))
Choice Validation
Restrict values to a set of valid choices:
(option name: 'level short: #\l long: "level"
value: "LEVEL" choices: '("debug" "info" "warn" "error")
description: "Log level")Invalid values produce an error. Help text shows: --level LEVEL [debug|info|warn|error]
Environment Variable Fallback
Use environment variables as fallback when option isn't provided:
(option name: 'token long: "token"
value: "TOKEN" env: "MY_APP_TOKEN"
description: "API token")Priority: command-line argument > environment variable > default value
Help text shows: --token TOKEN (env: MY_APP_TOKEN)
Flag Counting
Repeated flags are counted automatically:
(option name: 'verbose short: #\v
description: "Increase verbosity")Usage: -vvv
Result: (verbose . 3)
Single use returns #t, multiple uses return the count.
Defining Commands
Commands are defined with the command struct:
(define cmd
(command
name: "mytool"
description: "A useful tool"
options: (list ...)
handler: (lambda (opts args) ...)))Subcommands
Create nested command structures for complex CLIs:
(define build-cmd
(command
name: "build"
description: "Build the project"
options: (list
(option name: 'config short: #\c long: "config"
value: "NAME" default: "dev"
description: "Build configuration"))))
(define test-cmd
(command
name: "test"
description: "Run tests"
options: (list
(option name: 'filter short: #\f long: "filter"
value: "PATTERN"
description: "Filter tests by name"))))
(define main-cmd
(command
name: "mytool"
description: "Project management tool"
options: (list
(option name: 'verbose short: #\v long: "verbose"
description: "Enable verbose output"))
subcommands: (list build-cmd test-cmd)))Usage: mytool build --config release, mytool test --filter "unit*"
Parsing Arguments
Low-Level Parsing
Use parse-args to get a parse-result struct:
(let ((result (parse-args cmd args)))
(if (null? (parse-result-errors result))
(begin
(display "Options: ")
(write (parse-result-opts result))
(newline)
(display "Arguments: ")
(write (parse-result-args result))
(newline))
(begin
(display "Errors:\n")
(for-each (lambda (err)
(display " ")
(display err)
(newline))
(parse-result-errors result)))))The parse-result record contains:
opts- Alist of parsed option valuesargs- List of positional argumentserrors- List of error messages (empty if successful)subcommand- Matched subcommand record, or#f
High-Level Execution
Use run-command for automatic help, error handling, and handler execution:
(define (handle-main opts args)
(display "Running with: ")
(write opts)
(newline))
(run-command cmd handle-main (command-line-args))run-command automatically:
- Displays help when
-hor--helpis passed - Prints errors and exits on parse failures
- Calls the handler with parsed options and arguments
Accessing Parsed Options
Use alist-get (from (sigil core)) to retrieve option values:
(define (handle opts args)
(let ((verbose (alist-get 'verbose opts #f))
(output (alist-get 'output opts "default.txt"))
(count (alist-get 'count opts 1)))
...))The third argument is the default if the key isn't found.
Positional Arguments
Arguments that don't start with - are collected as positional arguments:
(let* ((result (parse-args cmd '("--verbose" "file1.txt" "file2.txt")))
(files (parse-result-args result)))
;; files is '("file1.txt" "file2.txt")
...)Use -- to stop option parsing and treat remaining arguments as positional:
;; mytool -- -v --help
;; args becomes '("-v" "--help"), not parsed as optionsGenerating Help
Automatic Help
generate-help creates formatted help text:
(display (generate-help cmd))Output:
mytool - A useful tool Usage: mytool [options] [command] Options: -v, --verbose Enable verbose output -h, --help Show this help message Commands: build Build the project test Run tests
Help Formatting Details
The help generator formats options intelligently:
| Option Type | Help Format | ||
|---|---|---|---|
| Flag with short and long | -v, --verbose | ||
| Option with value | -o, --output FILE | ||
| Negatable flag | --[no-]color | ||
| With choices | `--level LEVEL [debug | info | warn]` |
| With env fallback | --token TOKEN (env: MY_TOKEN) |
Complete Example
Here's a complete CLI application:
(import (sigil args)
(sigil io)
(sigil fs))
;; Build subcommand
(define build-cmd
(command
name: "build"
description: "Build the project"
options: (list
(option name: 'config short: #\c long: "config"
value: "NAME" default: "dev"
choices: '("dev" "debug" "release")
description: "Build configuration")
(option name: 'output short: #\o long: "output"
value: "DIR" default: "build"
description: "Output directory"))))
;; Test subcommand
(define test-cmd
(command
name: "test"
description: "Run tests"
options: (list
(option name: 'filter short: #\f long: "filter"
value: "PATTERN"
description: "Run tests matching pattern")
(option name: 'verbose short: #\v long: "verbose"
description: "Show detailed output"))))
;; Main command
(define main-cmd
(command
name: "myproject"
description: "Project build and test tool"
options: (list
(option name: 'verbose short: #\v long: "verbose"
description: "Enable verbose logging")
(option name: 'color long: "color" negatable: #t
description: "Enable colored output"))
subcommands: (list build-cmd test-cmd)))
;; Handlers
(define (handle-build opts args)
(let ((config (alist-get 'config opts))
(output (alist-get 'output opts)))
(display (string-append "Building with config: " config "\n"))
(display (string-append "Output directory: " output "\n"))))
(define (handle-test opts args)
(let ((filter (alist-get 'filter opts #f))
(verbose (alist-get 'verbose opts #f)))
(display "Running tests")
(when filter
(display (string-append " matching: " filter)))
(newline)))
(define (handle-main opts args)
(let ((result (parse-args main-cmd args)))
(cond
((parse-result-subcommand result)
=> (lambda (sub)
(cond
((string=? (command-name sub) "build")
(handle-build (parse-result-opts result)
(parse-result-args result)))
((string=? (command-name sub) "test")
(handle-test (parse-result-opts result)
(parse-result-args result))))))
(else
(print-help main-cmd)))))
;; Entry point
(run-command main-cmd handle-main (command-line-args))Running myproject --help produces:
myproject - Project build and test tool
Usage: myproject [options] [command]
Options:
-v, --verbose Enable verbose logging
--[no-]color Enable colored output
-h, --help Show this help message
Commands:
build Build the project
test Run testsRunning myproject build --help produces:
build - Build the project Usage: build [options] Options: -c, --config NAME Build configuration [dev|debug|release] (default: dev) -o, --output DIR Output directory (default: build) -h, --help Show this help message
Running myproject test --help produces:
test - Run tests Usage: test [options] Options: -f, --filter PATTERN Run tests matching pattern -v, --verbose Show detailed output -h, --help Show this help message
API Reference
Structs
| Struct | Purpose |
|---|---|
option | Defines a CLI option |
command | Defines a command or subcommand |
parse-result | Result of parsing |
Option Fields
| Field | Type | Description |
|---|---|---|
name | symbol | Key in parsed opts alist |
short | char | Short form: -x |
long | string | Long form: --name |
description | string | Help text |
value | string | Value placeholder (makes it value option vs flag) |
default | any | Default value |
required | boolean | Must be provided |
parse | procedure | Value transformer |
env | string | Environment variable fallback |
choices | list | Valid values |
multi | boolean | Accumulate repeated values |
negatable | boolean | Support --no-X form |
Functions
| Function | Purpose |
|---|---|
parse-args | Parse argv, return parse-result |
run-command | Parse, handle help/errors, call handler |
generate-help | Generate help text string |
print-help | Print help to stdout |
find-subcommand | Find subcommand by name |
Best Practices
- Use descriptive names: Option names should be clear without context
- Provide defaults: Most options should have sensible defaults
- Document choices: When using
choices:, list them in the description too - Use environment fallbacks for secrets: Tokens and keys should support env vars
- Group related options: Use subcommands for distinct functionality
- Keep handlers focused: Each handler should do one thing well