샘플
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
검증된 샘플 — cargo clap 4.6.6: Define and test a clap 4 derive CLI across the #[clap] to #[command] and #[arg] attribute split, checking outcomes with…
sha256:6e0aceb749a7c19cc2aa7a185f5f9cac427fedb08e50c9afc51b0c69de42d116
이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다.
통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다.
MIT-0
실행 증거
선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.
- 증거 기준
- 서명된 컨트랙트 통과
- 검증 영수증
- 3
- 빌드한 서명 키
- 2
선언된 환경
rust 1 linux x64 rust 1 rust cargo
검증 실행 환경
| 환경 | 컨트랙트 | 단계 | 실행일 |
|---|---|---|---|
| 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 |
케이스
HOW- 목표
- 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
- 패키지
- 심벌
-
- Parser::try_parse_from
- ArgAction::Count
- Command::build
- ErrorKind::UnknownArgument
- ErrorKind::MissingRequiredArgument
- ErrorKind::DisplayHelp
- ValueEnum
- CommandFactory::command
- 환경
- rust 1
- 생성일
- 2026-08-14T08:54:45Z
컨트랙트
- 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<T> 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
파일
- Cargo.lock
- Cargo.toml
- csx.json
- src/main.rs
소스
# 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",
]
[package]
name = "csx-sample-clap4"
version = "1.0.0"
edition = "2021"
license = "MIT-0"
[dependencies]
clap = { version = "4.6.6", features = ["derive"] }
{"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"}
//! 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");
}