sigildocs

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:

  • --color sets (color . #t)
  • --no-color sets (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 values
  • args - List of positional arguments
  • errors - 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 -h or --help is 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 options

Generating 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 TypeHelp Format
Flag with short and long-v, --verbose
Option with value-o, --output FILE
Negatable flag--[no-]color
With choices`--level LEVEL [debuginfowarn]`
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 tests

Running 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

StructPurpose
optionDefines a CLI option
commandDefines a command or subcommand
parse-resultResult of parsing

Option Fields

FieldTypeDescription
namesymbolKey in parsed opts alist
shortcharShort form: -x
longstringLong form: --name
descriptionstringHelp text
valuestringValue placeholder (makes it value option vs flag)
defaultanyDefault value
requiredbooleanMust be provided
parseprocedureValue transformer
envstringEnvironment variable fallback
choiceslistValid values
multibooleanAccumulate repeated values
negatablebooleanSupport --no-X form

Functions

FunctionPurpose
parse-argsParse argv, return parse-result
run-commandParse, handle help/errors, call handler
generate-helpGenerate help text string
print-helpPrint help to stdout
find-subcommandFind subcommand by name

Best Practices

  1. Use descriptive names: Option names should be clear without context
  2. Provide defaults: Most options should have sensible defaults
  3. Document choices: When using choices:, list them in the description too
  4. Use environment fallbacks for secrets: Tokens and keys should support env vars
  5. Group related options: Use subcommands for distinct functionality
  6. Keep handlers focused: Each handler should do one thing well