CodeSampleX

Exemplo

clap 4.6.6: Define and test a clap 4 derive CLI across the #[clap] to #[command] and #[arg] attribute split, checking outcomes with try_parse_from and ErrorKind instead of a process

Amostra verificada para cargo clap 4.6.6: Define and test a clap 4 derive CLI across the #[clap] to #[command] and #[arg] attribute split, checking outcomes…

sha256:6e0aceb749a7c19cc2aa7a185f5f9cac427fedb08e50c9afc51b0c69de42d116

Esta rede oferece uma coisa: uma amostra que compila. Ela a executou em um sandbox e guardou o recibo assinado. Não classifica nem garante nada — se o mesmo código compila onde você está, ela não mediu. Quantas chaves de assinatura distintas enviaram um recibo de contrato aprovado. Uma é só o autor; mais de uma significa que outra pessoa também o compilou. Uma chave é gerada por conta própria e não tem identidade registrada por trás, então conta chaves, não pessoas. MIT-0

Evidência de execução

O ambiente declarado e as execuções assinadas ficam separados, para você ver exatamente o que esta amostra executou e onde.

Base da evidência
Contrato assinado aprovado
Recibos de verificação
3
Chaves de assinatura que o compilaram
2
Ambiente declarado rust 1 linux x64 rust 1 rust cargo

Ambientes das execuções de verificação

Ambiente Contrato Etapas Execução
rust 1 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1
2026-08-14
rust 1 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · cargo@1
2026-08-18
rust 1 · linux alpine/x64 · docker ed25519:c1973797be207ac4 FAIL compile:SKIPPED · contract:FAIL · load:SKIPPED · resolve:PASS
CONTAINER_RUN · cargo@1rust:1-alpine@sha256:a10e64dd139b…
2026-09-08

Caso

HOW
Objetivo
Define and test a clap 4 derive CLI across the #[clap] to #[command] and #[arg] attribute split, checking outcomes with try_parse_from and ErrorKind instead of a process
Pacotes
Símbolos
  • Parser::try_parse_from
  • ArgAction::Count
  • Command::build
  • ErrorKind::UnknownArgument
  • ErrorKind::MissingRequiredArgument
  • ErrorKind::DisplayHelp
  • ValueEnum
  • CommandFactory::command
Ambiente
rust 1
Criado
2026-08-14T08:54:45Z

Contrato

  1. assert the command name comes from the attribute on the struct while every argument id comes from a field and its attribute
  2. assert help and version are absent from the Command until Command::build injects them, and that parsing performs that build for you
  3. assert the clap 3 #[clap(...)] spelling still compiles under clap 4, naming the command on the struct and declaring the argument on a field
  4. assert a minimal invocation fills default_value_t and leaves an Option<T> option as None
  5. assert dropping the program name from try_parse_from makes the first real argument the program name in the usage line and reports a missing required argument
  6. assert -o value, --output value, --output=value and the attached -ovalue all set the same option
  7. assert ArgAction::Count turns -vvv into 3 and -v -v into 2
  8. assert -V is the version flag while -v stays the verbose counter
  9. assert ValueEnum renders variants in kebab-case and that pretty_json, PrettyJson and JSON are ErrorKind::InvalidValue
  10. assert an unknown flag is ErrorKind::UnknownArgument and that the rendered error carries the tip naming the real --output
  11. assert a missing required positional is ErrorKind::MissingRequiredArgument
  12. assert both real errors exit 2 and go to stderr
  13. assert --help and -h are ErrorKind::DisplayHelp with exit code 0 on stdout, not a parse failure
  14. assert the rendered help shows the default value and the possible values
  15. assert --version is ErrorKind::DisplayVersion carrying the version from the command attribute

Arquivos

  • Cargo.lock
  • Cargo.toml
  • csx.json
  • src/main.rs

Baixar o artefato de código-fonte (tar.gz)

Código-fonte

Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "is_terminal_polyfill",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"

[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
 "windows-sys",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
 "anstyle",
 "once_cell_polyfill",
 "windows-sys",
]

[[package]]
name = "clap"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
 "clap_builder",
 "clap_derive",
]

[[package]]
name = "clap_builder"
version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
]

[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"

[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"

[[package]]
name = "csx-sample-clap4"
version = "1.0.0"
dependencies = [
 "clap",
]

[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"

[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"

[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"

[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"

[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"

[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"

[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
 "windows-link",
]
Cargo.toml
[package]
name = "csx-sample-clap4"
version = "1.0.0"
edition = "2021"
license = "MIT-0"

[dependencies]
clap = { version = "4.6.6", features = ["derive"] }
csx.json
{"case":{"caseId":"case:sha256:6c960a4cc0cc5e304b0a971283165502b5dae92450b3b104f1a7b786ea9f8cf0","constraints":{"runtime":"rust"},"contract":["assert the command name comes from the attribute on the struct while every argument id comes from a field and its attribute","assert help and version are absent from the Command until Command::build injects them, and that parsing performs that build for you","assert the clap 3 #[clap(...)] spelling still compiles under clap 4, naming the command on the struct and declaring the argument on a field","assert a minimal invocation fills default_value_t and leaves an Option\u003cT\u003e option as None","assert dropping the program name from try_parse_from makes the first real argument the program name in the usage line and reports a missing required argument","assert -o value, --output value, --output=value and the attached -ovalue all set the same option","assert ArgAction::Count turns -vvv into 3 and -v -v into 2","assert -V is the version flag while -v stays the verbose counter","assert ValueEnum renders variants in kebab-case and that pretty_json, PrettyJson and JSON are ErrorKind::InvalidValue","assert an unknown flag is ErrorKind::UnknownArgument and that the rendered error carries the tip naming the real --output","assert a missing required positional is ErrorKind::MissingRequiredArgument","assert both real errors exit 2 and go to stderr","assert --help and -h are ErrorKind::DisplayHelp with exit code 0 on stdout, not a parse failure","assert the rendered help shows the default value and the possible values","assert --version is ErrorKind::DisplayVersion carrying the version from the command attribute"],"goal":"Define and test a clap 4 derive CLI across the #[clap] to #[command] and #[arg] attribute split, checking outcomes with try_parse_from and ErrorKind instead of a process","kind":"HOW","packages":["pkg:cargo/clap@4.6.6"],"schemaVersion":1,"symbols":["Parser::try_parse_from","ArgAction::Count","Command::build","ErrorKind::UnknownArgument","ErrorKind::MissingRequiredArgument","ErrorKind::DisplayHelp","ValueEnum","CommandFactory::command"]},"contractCommand":["cargo","run","--offline","--quiet"],"environment":{"arch":"x64","ecosystem":"cargo","executionContext":"rust","language":"rust","os":"linux","packageManager":"cargo","runtime":"rust","runtimeVersion":"1","schemaVersion":1},"license":"MIT-0","packages":["pkg:cargo/clap@4.6.6"],"schemaVersion":1,"symbols":["Parser::try_parse_from","ArgAction::Count","Command::build","ErrorKind::UnknownArgument","ErrorKind::MissingRequiredArgument","ErrorKind::DisplayHelp","ValueEnum","CommandFactory::command"],"verifierAdapter":"cargo@1"}
src/main.rs
//! clap 4 with the derive feature, and how to test a CLI without a process.
//!
//! The clap 3 to 4 migration is usually told as "the single `#[clap(...)]`
//! attribute split into `#[command(...)]` for the Command and `#[arg(...)]`
//! for each argument". Measured against clap 4.6.6, that story is misleading
//! in the direction that costs time: the old spelling still compiles, with no
//! deprecation warning and no behaviour change. `#[clap(...)]` is an alias
//! whose target is decided by position — on the struct it configures the
//! Command, on a field it configures one argument — which is the thing the two
//! new names merely made explicit. LegacyCli below is written the clap 3 way
//! and parses like everything else here.
//!
//! What actually breaks is the contents of the attribute. `parse(...)` was
//! removed, so a clap 3 `#[clap(short, long, parse(from_occurrences))]` fails
//! with "cannot find value `from_occurrences` in this scope" and "no method
//! named `parse` found for struct `Arg`". The derive lowers attribute contents
//! into builder calls, so the compiler blames a value and a method you never
//! wrote and never mentions clap 3. Counting repetitions is
//! `action = ArgAction::Count` now.
//!
//! The second trap is testing. `Parser::parse()` reads the real argv and, on
//! bad input or `--help`, prints and calls process::exit — inside a test that
//! kills the whole run. `try_parse_from` takes the argv you hand it and
//! returns Result, so every outcome is a value you can assert on. Two things
//! about that argv: element 0 is the program name and clap skips it, and
//! `--help` comes back as `Err`. That Err is not a failure — its kind is
//! ErrorKind::DisplayHelp and its exit code is 0. Code that treats any Err
//! from try_parse_from as a parse error reports success as failure.

use clap::error::ErrorKind;
use clap::{ArgAction, CommandFactory, Parser, ValueEnum};

#[derive(Parser, Debug, PartialEq)]
#[command(name = "csxcat", version = "1.2.3", about = "Concatenate things")]
struct Cli {
    /// File to read. A plain field with no default is a required positional.
    path: String,

    /// Where to write. Option<T> is what makes an option optional.
    #[arg(short, long)]
    output: Option<String>,

    /// Worker count. default_value_t takes a real value of the field type;
    /// the older default_value takes a string.
    #[arg(short = 'j', long, default_value_t = 4)]
    jobs: u16,

    /// Repeatable: -v -vv -vvv. The field type is the counter, not bool.
    #[arg(short, long, action = ArgAction::Count)]
    verbose: u8,

    /// Constrained to the ValueEnum variants below.
    #[arg(long, value_enum, default_value_t = Format::Text)]
    format: Format,
}

#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum Format {
    Text,
    Json,
    PrettyJson,
}

/// The same CLI in clap 3 attribute style, kept here because it still works.
#[derive(Parser, Debug, PartialEq)]
#[clap(name = "csxcat", version = "1.2.3")]
struct LegacyCli {
    path: String,
    #[clap(short, long)]
    output: Option<String>,
    #[clap(short, long, action = ArgAction::Count)]
    verbose: u8,
}

fn arg_ids(cmd: &clap::Command) -> Vec<String> {
    cmd.get_arguments().map(|a| a.get_id().to_string()).collect()
}

fn main() {
    // Where an attribute sits is what decides its target: the name below came
    // from #[command(name = ...)] on the struct, and every id came from a field
    // plus its #[arg(...)].
    let mut cmd = Cli::command();
    assert_eq!(cmd.get_name(), "csxcat");
    let declared = arg_ids(&cmd);
    for want in ["path", "output", "jobs", "verbose", "format"] {
        assert!(declared.contains(&want.to_string()), "missing arg id {want}");
    }
    // Measured, and not what I expected: the Command handed back by
    // CommandFactory carries only the arguments you declared. --help and
    // --version are injected later, by Command::build. Introspecting
    // Cli::command() directly — to generate completions or docs — therefore
    // misses them unless you build it first.
    assert!(!declared.contains(&"help".to_string()));
    assert!(!declared.contains(&"version".to_string()));
    cmd.build();
    let built = arg_ids(&cmd);
    assert!(built.contains(&"help".to_string()));
    assert!(built.contains(&"version".to_string()));

    // Parsing performs that build for you, which is why nobody notices until
    // they introspect a Command they never parsed with.
    let mut parsed_with = Cli::command();
    let _ = parsed_with.try_get_matches_from_mut(["csxcat", "in.txt"]);
    let after_parse = arg_ids(&parsed_with);
    assert!(after_parse.contains(&"help".to_string()));
    assert!(after_parse.contains(&"version".to_string()));

    // The clap 3 spelling, still accepted at the same version that supposedly
    // replaced it: #[clap] on the struct named the command, #[clap] on a field
    // declared the argument, and ArgAction::Count works inside it. A file
    // half-migrated by hand gives you no signal that anything is stale.
    assert_eq!(LegacyCli::command().get_name(), "csxcat");
    assert_eq!(
        LegacyCli::try_parse_from(["csxcat", "-vv", "-o", "out.txt", "in.txt"]).unwrap(),
        LegacyCli {
            path: "in.txt".to_string(),
            output: Some("out.txt".to_string()),
            verbose: 2,
        }
    );

    // Element 0 is the program name. Defaults come from default_value_t.
    let cli = Cli::try_parse_from(["csxcat", "in.txt"]).expect("minimal invocation parses");
    assert_eq!(
        cli,
        Cli {
            path: "in.txt".to_string(),
            output: None,
            jobs: 4,
            verbose: 0,
            format: Format::Text,
        }
    );

    // Drop the program name and clap takes your first real argument as the
    // program name, so the required positional is suddenly missing. The one
    // tell is the usage line: it prints in.txt, not the name from #[command].
    let no_argv0 = Cli::try_parse_from(["in.txt"]).unwrap_err();
    assert_eq!(no_argv0.kind(), ErrorKind::MissingRequiredArgument);
    assert!(no_argv0.render().to_string().contains("Usage: in.txt <PATH>"));

    // short and long are the same argument, and clap accepts every spelling
    // of the value: separated, attached, and =.
    let sep = Cli::try_parse_from(["csxcat", "-o", "out.txt", "in.txt"]).unwrap();
    let long = Cli::try_parse_from(["csxcat", "--output", "out.txt", "in.txt"]).unwrap();
    let eq = Cli::try_parse_from(["csxcat", "--output=out.txt", "in.txt"]).unwrap();
    let attached = Cli::try_parse_from(["csxcat", "-oout.txt", "in.txt"]).unwrap();
    assert_eq!(sep.output.as_deref(), Some("out.txt"));
    assert_eq!(sep, long);
    assert_eq!(sep, eq);
    assert_eq!(sep, attached);

    // ArgAction::Count. -vvv is one argument occurring three times, not an
    // unknown flag named "vvv".
    assert_eq!(
        Cli::try_parse_from(["csxcat", "-vvv", "in.txt"])
            .unwrap()
            .verbose,
        3
    );
    assert_eq!(
        Cli::try_parse_from(["csxcat", "-v", "-v", "in.txt"])
            .unwrap()
            .verbose,
        2
    );
    // -v is ours; the version from #[command] claims capital -V. They coexist.
    assert_eq!(
        Cli::try_parse_from(["csxcat", "-V"]).unwrap_err().kind(),
        ErrorKind::DisplayVersion
    );

    // ValueEnum renders variants in kebab-case, so PrettyJson is the string
    // "pretty-json". Guessing "pretty_json" or "PrettyJson" is InvalidValue,
    // not a silent fallback to the default, and matching is case sensitive.
    let names: Vec<String> = Format::value_variants()
        .iter()
        .map(|v| v.to_possible_value().unwrap().get_name().to_string())
        .collect();
    assert_eq!(names, ["text", "json", "pretty-json"]);
    assert_eq!(
        Cli::try_parse_from(["csxcat", "--format", "pretty-json", "in.txt"])
            .unwrap()
            .format,
        Format::PrettyJson
    );
    for bad in ["pretty_json", "PrettyJson", "JSON"] {
        assert_eq!(
            Cli::try_parse_from(["csxcat", "--format", bad, "in.txt"])
                .unwrap_err()
                .kind(),
            ErrorKind::InvalidValue,
            "value {bad} should not be accepted"
        );
    }

    // A flag nobody declared. The rendered error carries a suggestion, which is
    // why the default features pull in a string-similarity crate. Asserting
    // only that the text contains --output would prove nothing here: the usage
    // line of this same error names --output as well.
    let unknown = Cli::try_parse_from(["csxcat", "--outpu", "x", "in.txt"]).unwrap_err();
    assert_eq!(unknown.kind(), ErrorKind::UnknownArgument);
    assert!(unknown
        .render()
        .to_string()
        .contains("tip: a similar argument exists: '--output'"));
    assert_eq!(
        Cli::try_parse_from(["csxcat", "-z", "in.txt"])
            .unwrap_err()
            .kind(),
        ErrorKind::UnknownArgument
    );

    // Nothing at all: the positional is required.
    let missing = Cli::try_parse_from(["csxcat"]).unwrap_err();
    assert_eq!(missing.kind(), ErrorKind::MissingRequiredArgument);

    // Both real errors exit 2 and belong on stderr.
    for err in [&unknown, &missing] {
        assert_eq!(err.exit_code(), 2);
        assert!(err.use_stderr());
    }

    // --help is an Err whose kind is DisplayHelp, exit code 0, destined for
    // stdout. It wins over the missing required positional, so a help request
    // never turns into a usage error. Match on kind(), never on is_err().
    for spelling in ["--help", "-h"] {
        let help = Cli::try_parse_from(["csxcat", spelling]).unwrap_err();
        assert_eq!(help.kind(), ErrorKind::DisplayHelp);
        assert_eq!(help.exit_code(), 0);
        assert!(!help.use_stderr());
        let text = help.render().to_string();
        assert!(text.contains("-o, --output <OUTPUT>"));
        assert!(text.contains("[default: 4]"));
        assert!(text.contains("[possible values: text, json, pretty-json]"));
    }

    let version = Cli::try_parse_from(["csxcat", "--version"]).unwrap_err();
    assert_eq!(version.kind(), ErrorKind::DisplayVersion);
    assert_eq!(version.exit_code(), 0);
    assert!(version.render().to_string().contains("csxcat 1.2.3"));

    println!("contract ok");
}

Seeder de origem

anonymous