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()parsesargsWith(spec)validates and normalizes
3. Normalized result shape
argsWith(spec) returns a normalized result object:
{
ok: true|false,
options: {...},
positional: [...],
errors: [...]
}
Rules:
okistruewhenerrorsis emptyoptionscontains canonical option names onlypositionalcontains positional values in ordererrorscontains 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 boband--user bobboth produceoptions.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:
typesdefaultsrequired- alias targets
desc
An explicit allow list may also be provided to further constrain accepted options.
Rules:
- if
allowis omitted, allowed options are inferred - if
allowis present, any option not inallowis 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:
strvalues pass through unchangednumvalues are parsed as numbers; invalid values are errorsboolvalues 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
optionswhen 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:
minis the minimum positional countmaxis the maximum positional count; if omitted, positional arguments are unboundednameis 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
msgis 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:
-hand--helpdo 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:
-vand--versiondo not need to be declared in the spec- the rendered output is derived from
nameandversion - if
versionis absent,--versionmay 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