diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 53b90ab..173b75e 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -10,6 +10,19 @@ //! a top-level `FromArgs` type from the current program's commandline //! arguments. //! +//! ## Table of Contents +//! +//! - [Basic Example](#basic-example) +//! - [Switches and Options](#switches-and-options) +//! - [Custom Option Types](#custom-option-types) +//! - [Positional Arguments](#positional-arguments) +//! - [Subcommands](#subcommands) +//! - [Dynamic Subcommands](#dynamic-subcommands) +//! - [Custom Help and Examples](#custom-help-and-examples) +//! - [Hidden Arguments](#hidden-arguments) +//! - [Skipped Fields](#skipped-fields) +//! - [Supported `#[argh(...)]` Attributes](#supported-argh-attributes) +//! //! ## Basic Example //! //! ```rust,no_run @@ -53,6 +66,8 @@ //! - `./some_bin -j --height 5` //! - `./some_bin --jump --height 5 --pilot-nickname Wes` //! +//! ## Switches and Options +//! //! Switches, like `jump`, are optional and will be set to true if provided. //! //! Options, like `height` and `pilot_nickname`, can be either required, @@ -90,6 +105,8 @@ //! } //! ``` //! +//! ## Custom Option Types +//! //! Custom option types can be deserialized so long as they implement the //! `FromArgValue` trait (automatically implemented for all `FromStr` types). //! If more customized parsing is required, you can supply a custom @@ -135,6 +152,8 @@ //! // > Error parsing option '--how' with value 'whatever': expected "soft_core" or "hard_core" //! ``` //! +//! ## Positional Arguments +//! //! Positional arguments can be declared using `#[argh(positional)]`. //! These arguments will be parsed in order of their declaration in //! the structure: @@ -180,6 +199,8 @@ //! before the rest of the arguments can be interpreted, and shouldn't be used //! for regular use as it might be confusing. //! +//! ## Subcommands +//! //! Subcommands are also supported. To use a subcommand, declare a separate //! `FromArgs` type for each subcommand as well as an enum that cases //! over each command: @@ -220,6 +241,8 @@ //! } //! ``` //! +//! ### Dynamic Subcommands +//! //! You can also discover subcommands dynamically at runtime. To do this, //! declare subcommands as usual and add a variant to the enum with the //! `dynamic` attribute. Instead of deriving `FromArgs`, the value inside the @@ -317,6 +340,8 @@ //! } //! ``` //! +//! ## Custom Help and Examples +//! //! You can define a complex help output that includes an **Examples** section. //! Use a `{command_name}` placeholder. //! @@ -375,6 +400,8 @@ //! goup --height 5 --pilot-nickname Wes jump //! ``` //! +//! ## Hidden Arguments +//! //! Programs that are run from an environment such as cargo may find it //! useful to have positional arguments present in the structure but //! omitted from the usage output. This can be accomplished by adding @@ -397,6 +424,82 @@ //! real_first_arg: String, //! } //! ``` +//! +//! ### Skipped Fields +//! +//! The `skip` attribute excludes a field from argument parsing entirely. Fields +//! marked `skip` do not appear in help or usage text and are never set from the +//! parsed command line arguments. Skipped fields are initialized using the value +//! passed by `default` attribute if one is provided, or simply `Default::default()` +//! otherwise. This is useful for fields that are part of a type but are/should be +//! populated by other means (for example, computed after parsing). +//! +//! ```rust +//! # use argh::FromArgs; +//! use std::sync::OnceLock; +//! +//! # type InternalState = (); +//! +//! #[derive(FromArgs)] +//! /// Reach new heights. +//! struct GoUp { +//! /// how high to go +//! #[argh(option)] +//! height: usize, +//! +//! // Never parsed from the command line; +//! // initialized with `Default::default()` +//! #[argh(skip)] +//! state: OnceLock, +//! } +//! ``` +//! +//! ## Supported `#[argh(...)]` Attributes +//! +//! ### Field-level attributes +//! +//! | Attribute | Description | Example | +//! | :-------------- | :----------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------- | +//! | `arg_name` | override the placeholder name shown for the value in usage/help | `#[argh(option, arg_name = "path")]` -> `--foo ` | +//! | `default` | fallback expression used when an `option`/`positional` is not supplied | `#[argh(option, default = "5")] foo: u32` | +//! | `description` | explicit help text for the field (usually supplied via a `///` doc comment instead) | `#[argh(description = "how high to go")]` | +//! | `from_str_fn` | custom parser `fn(&str) -> Result` for an `option`/`positional` | `#[argh(option, from_str_fn(parse_five))]` | +//! | `greedy` | make the final `positional` consume all remaining arguments, including flags | `#[argh(positional, greedy)]` | +//! | `hidden_help` | omit the argument from generated usage/help output | `#[argh(option, hidden_help)]` | +//! | `long` | override the `--`-prefixed long name (defaults to the kebab-cased field name) | `#[argh(option, long = "foo")]` -> `--foo` | +//! | `option` | mark the field as a `--key value` option; the field may be required, `Option`, or `Vec` (repeating) | `#[argh(option)] foo: u32` -> `--foo 5` | +//! | `positional` | mark the field as a positional argument, parsed in declaration order | `#[argh(positional)] foo: String` -> `bar` | +//! | `short` | add a single-character short alias for a `switch`/`option` | `#[argh(switch, short = 'f')]` -> `-f` | +//! | `skip` | unconditionally omit the field from parsing and help; initialized from `default` if present, else `Default::default()` | `#[argh(skip)] foo: MyType` | +//! | `subcommand` | mark the field as a subcommand enum (at most one per struct) | `#[argh(subcommand)] cmd: MyCmd` | +//! | `switch` | mark the field as an optional boolean switch, set to `true` by passing the flag | `#[argh(switch)] foo: bool` -> `--foo` | +//! | `usage` | opt a field into the explicit usage line (when at least one field is annotated, only annotated fields appear) | `#[argh(option, usage)]` | +//! +//! ### Type-level attributes +//! +//! | Attribute | Description | Example | +//! | :---------------- |:-------------------------------------------------------------------------------------- | :------------------------------------------------- | +//! | `description` | explicit help text for the command (usually supplied via a `///` doc comment instead) | `#[argh(description = "a tool")]` | +//! | `error_code` | document an exit code in the `Error codes:` help section | `#[argh(error_code(2, "file not found"))]` | +//! | `example` | add an entry to the `Examples:` help section (`{command_name}` is substituted) | `#[argh(example = "{command_name} --foo")]` | +//! | `help_triggers` | override the arguments that trigger help (defaults to `"--help"`, `"help"`) | `#[argh(help_triggers("-h", "--help", "help"))]` | +//! | `name` | the invoked name of a subcommand (required on subcommand structs) | `#[argh(name = "list")]` | +//! | `note` | add an entry to the `Notes:` help section | `#[argh(note = "some note")]` | +//! | `short` | single-character alias for a subcommand's name | `#[argh(short = 'l')]` | +//! | `subcommand` | mark a struct or enum as participating in subcommand dispatch | `#[argh(subcommand)]` | +//! | `usage` | fully override the generated usage line | `#[argh(usage = "--foo ")]` | +//! +//! ### Subcommand variant attributes +//! +//! | Attribute | Description | Example | +//! | :---------- | :--------------------------------------------------------------------------------------------------------- | :---------------------------- | +//! | `dynamic` | mark a subcommand enum variant as providing [dynamic subcommands](DynamicSubCommand) resolved at runtime | `#[argh(dynamic)] Foo(Foo)` | +//! +//! ### [`FromArgValue`] choice-enum variant attributes +//! +//! | Attribute | Description | Example | +//! | :---------- | :------------------------------------------------------------------------------------------------ | :----------------------------------------- | +//! | `name` | override the string that maps to this choice variant (defaults to the snake-cased variant name) | `#[argh(name = "read-write")] ReadWrite` | #![deny(missing_docs)] @@ -414,7 +517,6 @@ pub type CommandInfoWithArgs = argh_shared::CommandInfoWithArgs<'static>; pub type SubCommandInfo = argh_shared::SubCommandInfo<'static>; pub use argh_shared::{ErrorCodeInfo, FlagInfo, FlagInfoKind, Optionality, PositionalInfo}; - #[cfg(feature = "fuzzy_search")] use rust_fuzzy_search::fuzzy_search_best_n; diff --git a/argh/tests/args_info_tests.rs b/argh/tests/args_info_tests.rs index a1b458d..2d632ba 100644 --- a/argh/tests/args_info_tests.rs +++ b/argh/tests/args_info_tests.rs @@ -1071,3 +1071,73 @@ fn ok_hygiene() { input: String, } } + +#[test] +fn args_info_unit_and_struct_variants() { + /// hocus pocus + #[derive(Debug, PartialEq, FromArgs, ArgsInfo)] + #[argh(subcommand, name = "magick")] + struct MagicCommand; + + /// top-level command + #[derive(Debug, PartialEq, FromArgs, ArgsInfo)] + enum SomeCommand { + /// report info + Info, + + /// do magic + Magic(MagicCommand), + + /// create a macguffin + #[argh(name = "make")] + Create { + /// the macguffin name + #[argh(positional)] + macguffin: Option, + }, + } + + let info = get_info::(); + + // Top-level enum has empty name and the fixed subcommand description. + assert_eq!(info.name, ""); + assert_eq!(info.description, " enum of subcommands"); + + let names = info.commands.iter().map(|subcommand| subcommand.name).collect::>(); + + assert_eq!(names, vec!["info", "magick", "make"]); + + // Unit variant: no args beyond --help, name defaults to kebab-cased ident. + let unit = &info.commands[0].command; + + assert_eq!(unit.name, "info"); + assert_eq!(unit.flags, &[HELP_FLAG]); + assert_eq!(unit.description, "report info"); + assert_eq!(unit.positionals, &[] as &[PositionalInfo<'_>]); + + // Delegated variant defers to the unit struct's own info. + let magic = &info.commands[1].command; + + assert_eq!(magic.name, "magick"); + assert_eq!(magic.flags, &[HELP_FLAG]); + assert_eq!(magic.description, "hocus pocus"); + + // Struct-style variant exposes its own positional, honoring `name`. + let make = &info.commands[2].command; + + assert_eq!(make.name, "make"); + assert_eq!(make.description, "create a macguffin"); + assert_eq!( + make.positionals, + &[PositionalInfo { + hidden: false, + name: "macguffin", + description: "the macguffin name", + optionality: Optionality::Optional, + }] + ); +} + +fn get_info() -> CommandInfoWithArgs { + T::get_args_info() +} diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 9ef1774..5999968 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -11,10 +11,9 @@ clippy::unwrap_in_result )] -use { - argh::{FromArgValue, FromArgs}, - std::fmt::Debug, -}; +use std::fmt::Debug; + +use argh::{FromArgValue, FromArgs}; #[test] fn basic_example() { @@ -40,8 +39,7 @@ fn basic_example() { #[test] fn generic_example() { - use std::fmt::Display; - use std::str::FromStr; + use std::{fmt::Display, str::FromStr}; #[derive(FromArgs, PartialEq, Debug)] /// Reach new heights. @@ -468,6 +466,82 @@ fn assert_error(args: &[&str], err_msg: &str) { e.status.expect_err("error had a positive status"); } +mod skip { + use super::*; + + #[derive(Debug, Default, PartialEq)] + struct DefaultableValue { + inner: bool, + } + + #[derive(Debug, PartialEq, argh::FromArgs)] + /// SkipTest + struct WithSkipDefaultImpl { + #[argh(switch)] + /// foo bar baz + flag: bool, + /// skipped, falls back to `Default::default()` + #[argh(skip)] + skipped: DefaultableValue, + } + + #[derive(Debug, PartialEq, argh::FromArgs)] + /// SkipTest + struct WithSkipExplicitDefault { + #[argh(option)] + /// foo bar baz + option: usize, + /// skipped, uses the provided `default` + #[argh(skip, default = "DefaultableValue { inner: true }")] + skipped: DefaultableValue, + } + + #[test] + fn skip_uses_default_impl() { + assert_output( + &["--flag"], + WithSkipDefaultImpl { flag: true, skipped: DefaultableValue { inner: false } }, + ); + } + + #[test] + fn skip_honors_explicit_default() { + assert_output( + &["--option", "5"], + WithSkipExplicitDefault { option: 5, skipped: DefaultableValue { inner: true } }, + ); + } + + #[test] + fn skip_is_not_parsed_as_an_option() { + #[cfg(not(feature = "fuzzy_search"))] + let expected = "Unrecognized argument: --skipped\n"; + + #[cfg(feature = "fuzzy_search")] + let expected = "Unrecognized argument: \"--skipped\". Did you mean \"--option\"?\n"; + + assert_error::( + &["--option", "5", "--skipped", "whatever"], + expected, + ); + } + + #[test] + #[cfg(feature = "help")] + fn skip_is_omitted_from_help() { + assert_help_string::( + r#"Usage: test_arg_0 --option