Invisible link to canonical for Microformats

ADR-032L Declarative CLI Argument Specs via argsWith()


Status

Accepted

Context

Slug already provides low-level CLI argument access via argv() and map-style access via argm(), but programs still need to manually handle:

  • aliases
  • required options
  • default values
  • type coercion
  • unknown-option rejection
  • positional argument validation
  • help text generation
  • version string rendering

This leads to repeated boilerplate across CLI tools and increases the risk that parsing, validation, and help output drift apart over time.

We want a small, data-driven layer on top of argm() that preserves Slug’s design goals:

  • explicit behaviour
  • minimal surface area
  • predictable coercion and validation
  • reusable program metadata
  • a single source of truth for CLI UX

We also want help and version handling to come from the same specification so that command behaviour and command documentation remain consistent by construction.

Decision

Slug introduces a declarative CLI program spec consumed by a new helper:

argm() /> argsWith(spec)

argm() remains the raw parser. argsWith(spec) performs normalization, validation, coercion, and metadata-driven help/version behaviour.

1. Design model

The CLI spec is plain Slug data.

Example:

val spec = {
  name: "backup",
  version: "1.2.0",
  summary: "Backup files to a remote location",

  alias: { a:"all", u:"user" },
  required: ["user"],
  defaults: { all:false },
  types: { all:"bool", user:"str" },
  desc: {
    user: "User account to run as",
    all: "Process all files"
  },
  positional: {
    min: 1,
    max: 1,
    name: "file"
  }
}

This keeps the feature data-driven, testable, and easy to generate or inspect programmatically.

2. argm() remains the primitive

argm() continues to expose raw parsed arguments exactly as it does today.

argsWith(spec) is a library/helper layer that operates on that raw result.

This preserves separation of concerns:

  • argm() parses
  • argsWith(spec) validates and normalizes

3. Normalized result shape

argsWith(spec) returns a normalized result object:

{
  ok: true|false,
  options: {...},
  positional: [...],
  errors: [...]
}

Rules:

  • ok is true when errors is empty
  • options contains canonical option names only
  • positional contains positional values in order
  • errors contains structured error values

Example successful result:

{
  ok: true,
  options: { all:false, user:"evan" },
  positional: ["notes.txt"],
  errors: []
}

4. Canonical option names and aliases

Aliases map short names to canonical long names:

alias: { a:"all", u:"user" }

Rules:

  • alias keys are short option names without dashes
  • alias values are canonical option names
  • output is always normalized to canonical names
  • -u bob and --user bob both produce options.user

Aliases exist only for input ergonomics; they are never preserved in normalized output.

5. Allowed options

The accepted option set is derived from the spec.

Allowed canonical option names are inferred from:

  • types
  • defaults
  • required
  • alias targets
  • desc

An explicit allow list may also be provided to further constrain accepted options.

Rules:

  • if allow is omitted, allowed options are inferred
  • if allow is present, any option not in allow is rejected
  • unknown options are errors by default

This makes strict CLI behaviour the default.

6. Type coercion

argsWith(spec) supports a deliberately small initial type set:

  • "bool"
  • "str"
  • "num"

Rules:

  • str values pass through unchanged
  • num values are parsed as numbers; invalid values are errors
  • bool values are normalized to actual booleans

Accepted boolean forms:

  • presence-only flag
  • "true"
  • "false"

Example:

--all
--all=true
--all=false
-a

all normalize to options.all as a boolean value.

7. Defaults and required options

Defaults are applied after normalization and before final validation.

Rules:

  • defaulted values are inserted into options when not supplied
  • required options must be present after alias resolution and default application
  • missing required options produce validation errors

This allows defaults and required checks to operate on canonical option names only.

8. Positional argument validation

The spec may declare positional constraints:

positional: {
  min: 1,
  max: 1,
  name: "file"
}

Rules:

  • min is the minimum positional count
  • max is the maximum positional count; if omitted, positional arguments are unbounded
  • name is used in usage/help/error messages
  • a future extension may support names: [...] for fixed multi-positional commands

This keeps positional validation simple while still enabling good diagnostics.

9. Structured errors

Validation failures are returned as structured error values, not just strings.

Example shapes:

[
  { type:"missing-required", option:"user", msg:"missing required option --user" },
  { type:"unknown-option", option:"usr", msg:"unknown option --usr" },
  { type:"invalid-type", option:"all", expected:"bool", value:"maybe", msg:"invalid bool for --all" },
  { type:"positional-min", min:1, actual:0, msg:"expected at least 1 positional argument: file" }
]

Rules:

  • errors must be deterministic and machine-readable
  • msg is included for direct user display
  • helper formatting functions may be added for pretty CLI output

10. Automatic --help

When a spec includes command metadata such as name, summary, desc, and positional metadata, help output can be generated directly from the same source of truth.

argsWith(spec) recognizes -h and --help automatically.

Rules:

  • -h and --help do not need to be declared in the spec
  • help output is generated from spec data
  • option descriptions, required markers, defaults, and positional usage come from the same spec used for validation

Example output shape:

backup 1.2.0

Backup files to a remote location

Usage:
  backup [options] <file>

Options:
  -u, --user <str>   User account to run as (required)
  -a, --all          Process all files (default: false)

Arguments:
  file               input file

This keeps parsing behaviour and help text aligned by construction.

11. Automatic --version

If the spec contains:

version: "1.2.0"

then argsWith(spec) recognizes -v and --version automatically.

Rules:

  • -v and --version do not need to be declared in the spec
  • the rendered output is derived from name and version
  • if version is absent, --version may be rejected or produce no special handling, as documented by the implementation

Example output:

backup 1.2.0

12. Single source of truth

The CLI spec is the authoritative source for:

  • accepted options
  • aliases
  • coercion rules
  • defaults
  • required validation
  • positional rules
  • help rendering
  • version rendering

This is the core design goal of the feature.

13. Initial non-goals

The following are explicitly out of scope for v1:

  • nested subcommands
  • custom validator functions
  • environment-variable binding
  • config-file merging
  • rich type grammars such as list[str]
  • automatic manpage generation
  • automatic shell completion generation

These may be added later without changing the core model.

Consequences

Positive

  • Eliminates repetitive CLI validation boilerplate
  • Keeps parsing, validation, help, and version output in sync
  • Preserves args() as a minimal primitive
  • Makes CLI behaviour declarative and testable
  • Produces machine-readable errors suitable for tooling
  • Improves consistency across Slug command-line programs
  • Creates a strong foundation for future doc/help/manpage tooling
  • Makes CLI specs easier for AI tools to inspect and use

Negative

  • Introduces a new spec shape that users must learn
  • Adds implementation complexity around coercion and error reporting
  • Automatic help/version handling introduces a small amount of implicit behaviour
  • Strict unknown-option rejection may require explicit migration for existing loose CLIs

Neutral

  • argm() remains useful as a lower-level map-oriented helper
  • The initial type system is intentionally small and may expand later
  • Formatting of generated help text is implementation-defined, but the information source is fixed
  • Future extensions may collapse some parallel maps (types, defaults, desc) into a more compact option schema, but v1 keeps the format flat and simple