Validation
Everything on this page runs after argv is bound and env/default fallbacks are applied. Only the command that actually ran is judged. Contradictory declarations — choices on a bool, var_min greater than var_max, a default that isn't one of the choices — are compile errors, not runtime surprises.
Choices and bounds
/// Output format
#[usage(long, choices("json", "table"))]
format: Option<String>,
/// Patterns to include
#[usage(long, var_min = 1, var_max = 5)]
include: Vec<String>,A value outside the set is Error::InvalidChoice { name, choices }; too few or too many values are VarTooFew/VarTooMany. For enum-shaped values prefer ValueEnum.
Flag relations
conflicts, requires, required_if, required_if_eq, and required_unless relate arguments to another. Targets are named the way the KDL spec names them ("--long" or "-s" for flags, or a bare positional name), and naming one that doesn't exist is a compile error:
/// Read from standard input
#[usage(long, conflicts("--file", "--url"))]
stdin: bool,
/// Retry count
#[usage(long, required_if("--retry"))]
max_retries: Option<u32>,overrides is the quiet sibling of conflicts: a later occurrence of one flag discards an earlier occurrence of the other instead of erroring — useful for --json / --yaml pairs where the last one typed should win. What it discards is the value, not the fact that a word was typed: a value outside the loser's choices is still an InvalidChoice, since overrides answers which flag is in effect and not whether the word was one that flag accepts. Only a later occurrence of the same flag replaces its value.
Groups
A group relates several flags at once: membership is declared on each field, the group's properties on the struct.
#[derive(Cli)]
#[usage(bin = "grp")]
#[usage(group("input", required))]
struct Grp {
/// Read from a file
#[usage(long, group = "input")]
file: Option<String>,
/// Read from a URL
#[usage(long, group = "input")]
url: Option<String>,
/// Read from standard input
#[usage(short = 's', long, group = "input")]
stdin: bool,
}The two properties compose the way clap's do:
| Declaration | Meaning |
|---|---|
group("name") | at most one |
group("name", required) | exactly one |
group("name", required, multiple) | at least one |
An unsatisfied required group is Error::MissingGroup { group, members }, rendered as clap renders it:
error: one of the following required arguments was not provided (input):
--file <PATH>
--url <URL>
-s, --stdinTwo members of a non-multiple group produce ConflictingFlags — matched by flag, not spelling, so giving one member as -s and another as --file still counts. A conflict is reported before an unsatisfied group.
Groups are emitted into the KDL spec (group input --file --url --stdin required=#true), and a group declared on a flattened struct is enforced on every command that flattens it. Malformed groups — one member, no members, declared twice, an unresolved member — are compile errors.
A group as an enum
The group can be the type instead: an enum deriving ArgGroup, whose variants are the flags. The code that reads it then matches on a variant rather than working out which of several fields is set.
/// How to print the result
#[derive(ArgGroup)]
#[usage(name = "format")]
enum Format {
/// Print JSON
Json,
/// Print YAML
Yaml,
/// Print one line per record
#[usage(short = 'p', long = "plain")]
PlainText,
}
#[derive(Cli)]
#[usage(bin = "fmt")]
struct Fmt {
#[usage(arg_group)]
format: Option<Format>,
}Unit variants are switches. A tuple variant with one field is a value-taking flag:
#[derive(ArgGroup)]
enum Mode {
Write,
Check,
#[usage(value_name = "SOURCE", value_enum)]
Migrate(MigrationSource),
#[usage(value_name = "PATH")]
StdinFilepath(std::path::PathBuf),
}Each member is named by its variant in kebab-case unless long or name says otherwise. short, hide, help and long_help apply to both forms; value_name and value_enum describe a tuple variant's payload. A doc comment is the help. Without #[usage(name = "…")] the group is named after the type.
The field's type says whether the group is required, exactly as it does everywhere else: Option<Format> is a group that may be left alone, and a bare Format is one that has to be given. There is no default variant, so that distinction is the only spelling of required-ness a group has.
A group whose occurrences form an ordered instruction stream declares multiple and is held by Vec<T>. Every member may repeat, and variants from different members remain interleaved exactly as they appeared in argv:
#[derive(ArgGroup)]
#[usage(name = "lint-filter", multiple)]
enum LintFilter {
#[usage(short = 'A')]
Allow(String),
#[usage(short = 'W')]
Warn(String),
#[usage(short = 'D')]
Deny(String),
}
#[derive(Cli)]
struct Lint {
#[usage(arg_group)]
filters: Vec<LintFilter>,
}Parsing -D all -A no-debugger produces vec![LintFilter::Deny("all"), LintFilter::Allow("no-debugger")]. This differs from three independent Vec<String> fields, which preserve order within each flag but lose their order relative to one another.
The enum lowers to the same group node (group format --json --yaml --plain), so --json --yaml is the same ConflictingFlags a hand-written group produces, a required group with none of its members given is the same MissingGroup, and help, docs and completions list the member flags without knowing an enum was involved.
A payload converts through FromStr, preserving non-UTF-8 bytes for PathBuf and OsString. With value_enum, its choices, aliases, help and case policy reach validation, help, generated specs and completions from the same enum declaration.
Exclusive flags
An exclusive flag has to be given alone — no other flag, no argument, no subcommand:
/// Dump the spec and leave
#[usage(long, exclusive)]
dump: bool,Use exclusive for an operation that must run alone, without listing every other argument as a conflict. The rules are:
--dump -vand--dump somefileboth fail withConflictingFlags.- An exclusive flag bypasses required-ness: required siblings the flag's command declares are not demanded when the exclusive flag is given — the
--version-style escape hatch. - Declared defaults still apply; only values the user actually supplied count as company — but a value supplied via
envdoes count. - Exclusivity crosses command boundaries in both directions: selecting a subcommand is company for a parent's exclusive flag, and a parent's flags are company for a child's.
exclusive on a positional is a compile error. Emitted KDL: flag "--dump" exclusive=#true.
Delimiters
delimiter splits one word into several values, the way clap's value_delimiter does:
/// Tags to apply
#[usage(long, delimiter = ',', var_max = 3)]
tags: Vec<String>,--tags a,b,c yields ["a", "b", "c"], and occurrences accumulate: --tags a,b --tags c is ["a", "b", "c"]. It works on positionals too (#[usage(arg, delimiter = ';')]).
The split runs after env fallback and before every check, so choices judges each split value and var_min/var_max count values, not words — --tags a,b,c,d with var_max = 3 is VarTooMany { got: 4 }.
The field must be a Vec, and the delimiter must be a single ASCII character; both are enforced at compile time. In KDL: flag "--tags <tag>" var=#true delimiter=",".
Cross-field validation and typed finalization
Use validate_with for an invariant that needs the fully typed root command rather than one field's text. It runs after field conversion and environment/default resolution on every parse path:
use usage::{Cli, ValidationError};
#[derive(Cli)]
#[usage(bin = "copy", validate_with = validate_copy)]
struct CopyArgs {
#[usage(long)]
source: std::path::PathBuf,
#[usage(long)]
destination: std::path::PathBuf,
}
fn validate_copy(args: &CopyArgs) -> Result<(), ValidationError> {
if args.source == args.destination {
return Err(ValidationError::field("--destination")
.value(args.destination.display().to_string())
.reason("must differ from --source"));
}
Ok(())
}The error is returned as the ordinary Error::InvalidValue, so renderers and embedding code do not need a second diagnostic path. Derive Clone when using the update entry points: updates are applied to a clone, validated, and committed only on success, so a failed update leaves the standing value untouched.
For applications that keep their parser declaration separate from the type passed to the rest of the program, try_into generates finalizing parse entry points:
#[derive(Cli)]
#[usage(bin = "copy", try_into = CopyCommand)]
struct CopyArgs { /* flags and arguments */ }
struct CopyCommand(CopyArgs);
impl TryFrom<CopyArgs> for CopyCommand {
type Error = usage::ValidationError;
fn try_from(args: CopyArgs) -> Result<Self, Self::Error> {
// Resolve modes, normalize paths, or establish richer invariants here.
Ok(Self(args))
}
}
fn main() {
let command: CopyCommand = CopyArgs::parse_into();
}parse_into_from, parse_into_from_with_warnings, parse_into_from_argv, and try_parse_into_from are the returning-error counterparts, and embedded_outcome_into is the embedded one. The original parse_* methods remain available when a caller wants the declaration type itself.
Portable expressions
For a rule that must survive KDL emission — and that clap would have expressed with a value_parser callback — enable the validation feature and declare an expression:
[dependencies]
usage = { package = "usage-rs", version = "6", features = ["validation"] }#[usage(
long,
validate = "int(value) >= 1 && int(value) <= 65535",
validate_error = "must be a valid port"
)]
port: Option<u16>,validate is evaluated after binding. validate_error is the diagnostic users see when the expression is false. Rust value_parser functions are not portable metadata — use FromStr for typed conversion and these attributes for declarative rules that docs, manpages, and other spec consumers can see.