Three layers: type coercion (automatic), per-param validation (inline), and cross-param validation (after all args are parsed).
Per-param: choices
The cheapest form. Enumerate the accepted values:
option :format, type: :string, choices: ["json", "csv", "table"]Rejected values produce error: --format must be one of: json, csv, table.
Also works on arguments:
argument :env, type: :string, choices: ["dev", "staging", "prod"]Per-param: inline validator
Any function that returns :ok or {:error, message}:
option :port, type: :integer,
validate: fn p ->
if p in 1024..65_535, do: :ok, else: {:error, "port must be 1024-65535"}
endRuns after type coercion, so the value you receive is already the declared type.
Cross-param validators
Run after every option and argument has been parsed. Receives the full
args map.
validate fn args ->
cond do
args[:tls] and is_nil(args[:cert]) -> {:error, "--tls requires --cert"}
args[:since] && args[:until] && args[:since] > args[:until] ->
{:error, "--since must be before --until"}
true -> :ok
end
endYou can declare multiple validate blocks per command. They run in
declaration order; the first {:error, ...} halts the pipeline.
Order of evaluation
For a single invocation, validation runs in this order:
- Type coercion (automatic, based on
:type). - Defaults and env var fallback.
- Required-argument and required-option checks.
- Choices (
:choices). - Per-param
:validatefunctions. - Cross-param validators (
validate fn args -> ... end). - Conditional-required (
:required_if,:required_unless). - Per-option constraints (
:conflicts_with,:requires). - Group constraints (
mutually_exclusive,co_occurring).
The first failure halts; your run/2 is only called if everything passes.
See also
- Constraints for conditional-required, conflicts, requires, and groups.
- Options and Arguments for the declarations validation operates on.