use facet::Facet;
use facet_args::{self as args, ArgsErrorWithInput};
use facet_showcase::{Language, Scenario, ShowcaseRunner};
trait ArgsResultExt<'a, 'b, T: facet::Facet<'b>> {
fn args_result(self, result: &'b Result<T, ArgsErrorWithInput>) -> Self;
}
impl<'a, 'b, T: facet::Facet<'b>> ArgsResultExt<'a, 'b, T> for Scenario<'a> {
fn args_result(self, result: &'b Result<T, ArgsErrorWithInput>) -> Self {
match result {
Ok(value) => self.success(value),
Err(err) if err.is_help_request() => {
self.ansi_output(err.help_text().unwrap_or(""))
}
Err(err) => {
self.ansi_output(&err.to_ariadne_string())
}
}
}
}
#[derive(Facet, Debug)]
struct SimpleArgs {
#[facet(args::named, args::short = 'v')]
verbose: bool,
#[facet(default, args::named, args::short = 'j')]
jobs: Option<usize>,
#[facet(args::positional)]
input: String,
#[facet(default, args::positional)]
output: Option<String>,
}
#[derive(Facet, Debug)]
struct GitLikeArgs {
#[facet(args::named)]
version: bool,
#[facet(args::subcommand)]
command: GitCommand,
}
#[derive(Facet, Debug)]
#[repr(u8)]
#[allow(dead_code)]
enum GitCommand {
Clone {
#[facet(args::positional)]
url: String,
#[facet(default, args::positional)]
directory: Option<String>,
#[facet(default, args::named, args::short = 'b')]
branch: Option<String>,
#[facet(default, args::named)]
depth: Option<usize>,
},
Status {
#[facet(args::named, args::short = 's')]
short: bool,
#[facet(args::named, args::short = 'b')]
branch: bool,
},
Remote {
#[facet(args::subcommand)]
action: RemoteAction,
},
}
#[derive(Facet, Debug)]
#[repr(u8)]
#[allow(dead_code)]
enum RemoteAction {
Add {
#[facet(args::positional)]
name: String,
#[facet(args::positional)]
url: String,
},
#[facet(rename = "rm")]
Remove {
#[facet(args::positional)]
name: String,
},
#[facet(rename = "ls")]
List {
#[facet(args::named, args::short = 'v')]
verbose: bool,
},
}
#[derive(Facet, Debug)]
struct BuildArgs {
#[facet(args::named, args::short = 'r')]
release: bool,
#[facet(default, args::named, args::short = 'j')]
jobs: Option<usize>,
#[facet(default, args::named, args::short = 'p')]
package: Option<String>,
#[facet(args::named)]
workspace: bool,
#[facet(default, args::named, args::short = 'F')]
features: Option<String>,
#[facet(default, args::named)]
target: Option<String>,
}
fn main() {
let mut runner = ShowcaseRunner::new("Args").language(Language::Rust);
runner.header();
runner.intro("[`facet-args`](https://docs.rs/facet-args) turns any `Facet` struct into a command-line interface. Define your CLI with doc comments and attributes like `args::named`, `args::positional`, and `args::subcommand`. Get auto-generated help text, shell completions for bash/zsh/fish, and rich error diagnostics with typo suggestions.");
runner.section("Successful Parsing");
showcase_simple_parsing(&mut runner);
showcase_attached_short_value(&mut runner);
showcase_bool_equals_value(&mut runner);
showcase_short_flag_chaining(&mut runner);
showcase_subcommand_parsing(&mut runner);
showcase_nested_subcommands(&mut runner);
runner.section("Help Generation");
showcase_help_simple(&mut runner);
showcase_help_auto_detection(&mut runner);
showcase_help_subcommands(&mut runner);
runner.section("Shell Completions");
showcase_completions_bash(&mut runner);
showcase_completions_zsh(&mut runner);
showcase_completions_fish(&mut runner);
runner.section("Error Diagnostics");
scenario_unknown_flag(&mut runner);
scenario_unknown_flag_suggestion(&mut runner);
scenario_invalid_short_flag_in_chain(&mut runner);
scenario_triple_dash(&mut runner);
scenario_single_dash_long_name(&mut runner);
scenario_missing_value(&mut runner);
scenario_missing_required_arg(&mut runner);
scenario_unexpected_positional(&mut runner);
scenario_unknown_subcommand(&mut runner);
scenario_missing_subcommand(&mut runner);
scenario_missing_nested_subcommand_arg(&mut runner);
scenario_invalid_value(&mut runner);
runner.footer();
}
fn showcase_simple_parsing(runner: &mut ShowcaseRunner) {
runner
.scenario("Simple Arguments")
.description("Parse a struct with flags, options, and positional arguments.")
.target_type::<SimpleArgs>()
.input(
Language::Rust,
"from_slice(&[\"-v\", \"-j\", \"4\", \"input.txt\", \"output.txt\"])",
)
.args_result(&args::from_slice::<SimpleArgs>(&[
"-v",
"-j",
"4",
"input.txt",
"output.txt",
]))
.finish();
}
fn showcase_attached_short_value(runner: &mut ShowcaseRunner) {
runner
.scenario("Attached Short Flag Value")
.description("Short flags can have their values attached directly without a space.")
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"-j4\", \"input.txt\"])")
.args_result(&args::from_slice::<SimpleArgs>(&["-j4", "input.txt"]))
.finish();
}
fn showcase_bool_equals_value(runner: &mut ShowcaseRunner) {
runner
.scenario("Boolean Flag with Explicit Value")
.description("Boolean flags can be explicitly set to true or false using `=`.")
.target_type::<SimpleArgs>()
.input(
Language::Rust,
"from_slice(&[\"--verbose=true\", \"input.txt\"])",
)
.args_result(&args::from_slice::<SimpleArgs>(&[
"--verbose=true",
"input.txt",
]))
.finish();
}
fn showcase_subcommand_parsing(runner: &mut ShowcaseRunner) {
runner
.scenario("Subcommands")
.description("Parse a CLI with subcommands, each with their own arguments.")
.target_type::<GitLikeArgs>()
.input(
Language::Rust,
"from_slice(&[\"clone\", \"--branch\", \"main\", \"https://github.com/user/repo\"])",
)
.args_result(&args::from_slice::<GitLikeArgs>(&[
"clone",
"--branch",
"main",
"https://github.com/user/repo",
]))
.finish();
}
fn showcase_nested_subcommands(runner: &mut ShowcaseRunner) {
runner
.scenario("Nested Subcommands")
.description("Parse deeply nested subcommands like `git remote add`.")
.target_type::<GitLikeArgs>()
.input(
Language::Rust,
"from_slice(&[\"remote\", \"add\", \"origin\", \"https://github.com/user/repo\"])",
)
.args_result(&args::from_slice::<GitLikeArgs>(&[
"remote",
"add",
"origin",
"https://github.com/user/repo",
]))
.finish();
}
fn showcase_short_flag_chaining(runner: &mut ShowcaseRunner) {
runner
.scenario("Short Flag Chaining")
.description(
"Multiple boolean short flags can be combined: `-sb` is equivalent to `-s -b`.",
)
.target_type::<GitLikeArgs>()
.input(Language::Rust, "from_slice(&[\"status\", \"-sb\"])")
.args_result(&args::from_slice::<GitLikeArgs>(&["status", "-sb"]))
.finish();
}
fn showcase_help_simple(runner: &mut ShowcaseRunner) {
let config = args::HelpConfig {
program_name: Some("mytool".to_string()),
version: Some("1.0.0".to_string()),
..Default::default()
};
let help = args::generate_help::<SimpleArgs>(&config);
runner
.scenario("Simple Help")
.description("Auto-generated help text from struct definition and doc comments.")
.target_type::<SimpleArgs>()
.ansi_output(&help)
.finish();
}
fn showcase_help_auto_detection(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["--help"]);
runner
.scenario("Automatic --help Detection")
.description("When `-h`, `--help`, `-help`, or `/?` is the first argument, help is automatically generated and returned.")
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"--help\"])")
.args_result(&result)
.finish();
}
fn showcase_help_subcommands(runner: &mut ShowcaseRunner) {
let config = args::HelpConfig {
program_name: Some("git".to_string()),
version: Some("2.40.0".to_string()),
..Default::default()
};
let help = args::generate_help::<GitLikeArgs>(&config);
runner
.scenario("Help with Subcommands")
.description("Help text automatically lists available subcommands with descriptions.")
.target_type::<GitLikeArgs>()
.ansi_output(&help)
.finish();
}
fn showcase_completions_bash(runner: &mut ShowcaseRunner) {
let completions = args::generate_completions::<BuildArgs>(args::Shell::Bash, "cargo-build");
runner
.scenario("Bash Completions")
.description("Generated Bash completion script for tab-completion support.")
.target_type::<BuildArgs>()
.serialized_output(Language::Plain, &completions)
.finish();
}
fn showcase_completions_zsh(runner: &mut ShowcaseRunner) {
let completions = args::generate_completions::<BuildArgs>(args::Shell::Zsh, "cargo-build");
runner
.scenario("Zsh Completions")
.description("Generated Zsh completion script with argument descriptions.")
.target_type::<BuildArgs>()
.serialized_output(Language::Plain, &completions)
.finish();
}
fn showcase_completions_fish(runner: &mut ShowcaseRunner) {
let completions = args::generate_completions::<BuildArgs>(args::Shell::Fish, "cargo-build");
runner
.scenario("Fish Completions")
.description("Generated Fish shell completion script.")
.target_type::<BuildArgs>()
.serialized_output(Language::Plain, &completions)
.finish();
}
fn scenario_unknown_flag(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["--verbos", "input.txt"]);
runner
.scenario("Unknown Flag")
.description("Error when an unrecognized flag is provided.")
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"--verbos\", \"input.txt\"])")
.args_result(&result)
.finish();
}
fn scenario_unknown_flag_suggestion(runner: &mut ShowcaseRunner) {
let result: Result<BuildArgs, _> = args::from_slice(&["--releas"]);
runner
.scenario("Unknown Flag with Suggestion")
.description("When the flag name is close to a valid one, a suggestion is offered.")
.target_type::<BuildArgs>()
.input(Language::Rust, "from_slice(&[\"--releas\"])")
.args_result(&result)
.finish();
}
fn scenario_invalid_short_flag_in_chain(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["-vxyz", "input.txt"]);
runner
.scenario("Invalid Short Flag in Chain")
.description(
"When chaining short flags, an unknown flag is reported with available options.",
)
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"-vxyz\", \"input.txt\"])")
.args_result(&result)
.finish();
}
fn scenario_triple_dash(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["---verbose", "input.txt"]);
runner
.scenario("Triple Dash Flag")
.description("Flags with too many dashes are rejected.")
.target_type::<SimpleArgs>()
.input(
Language::Rust,
"from_slice(&[\"---verbose\", \"input.txt\"])",
)
.args_result(&result)
.finish();
}
fn scenario_single_dash_long_name(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["-verbose", "input.txt"]);
runner
.scenario("Single Dash with Long Name")
.description("Long flag names require double dashes.")
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"-verbose\", \"input.txt\"])")
.args_result(&result)
.finish();
}
fn scenario_missing_value(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["-j"]);
runner
.scenario("Missing Value")
.description("Error when a flag that requires a value doesn't get one.")
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"-j\"])")
.args_result(&result)
.finish();
}
fn scenario_missing_required_arg(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["-v"]);
runner
.scenario("Missing Required Argument")
.description("Error when a required positional argument is not provided.")
.target_type::<SimpleArgs>()
.input(Language::Rust, "from_slice(&[\"-v\"])")
.args_result(&result)
.finish();
}
fn scenario_unexpected_positional(runner: &mut ShowcaseRunner) {
let result: Result<BuildArgs, _> = args::from_slice(&["extra", "--release"]);
runner
.scenario("Unexpected Positional Argument")
.description("Error when a positional argument is provided but not expected.")
.target_type::<BuildArgs>()
.input(Language::Rust, "from_slice(&[\"extra\", \"--release\"])")
.args_result(&result)
.finish();
}
fn scenario_unknown_subcommand(runner: &mut ShowcaseRunner) {
let result: Result<GitLikeArgs, _> = args::from_slice(&["clon", "https://example.com"]);
runner
.scenario("Unknown Subcommand")
.description(
"Error when an unrecognized subcommand is provided, with available options listed.",
)
.target_type::<GitLikeArgs>()
.input(
Language::Rust,
"from_slice(&[\"clon\", \"https://example.com\"])",
)
.args_result(&result)
.finish();
}
fn scenario_missing_subcommand(runner: &mut ShowcaseRunner) {
let result: Result<GitLikeArgs, _> = args::from_slice(&["--version"]);
runner
.scenario("Missing Subcommand")
.description("Error when a required subcommand is not provided.")
.target_type::<GitLikeArgs>()
.input(Language::Rust, "from_slice(&[\"--version\"])")
.args_result(&result)
.finish();
}
fn scenario_missing_nested_subcommand_arg(runner: &mut ShowcaseRunner) {
let result: Result<GitLikeArgs, _> = args::from_slice(&["remote", "add", "origin"]);
runner
.scenario("Missing Nested Subcommand Argument")
.description("Error when a required argument in a nested subcommand is missing.")
.target_type::<GitLikeArgs>()
.input(
Language::Rust,
"from_slice(&[\"remote\", \"add\", \"origin\"])",
)
.args_result(&result)
.finish();
}
fn scenario_invalid_value(runner: &mut ShowcaseRunner) {
let result: Result<SimpleArgs, _> = args::from_slice(&["-j", "not-a-number", "input.txt"]);
runner
.scenario("Invalid Value Type")
.description("Error when a value cannot be parsed as the expected type.")
.target_type::<SimpleArgs>()
.input(
Language::Rust,
"from_slice(&[\"-j\", \"not-a-number\", \"input.txt\"])",
)
.args_result(&result)
.finish();
}