diff options
author | Andrew Walbran <qwandor@google.com> | 2022-01-21 13:46:38 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-01-21 13:46:38 +0000 |
commit | 08526c8a78b4e7b0374284635f8a9009c60c0f34 (patch) | |
tree | 83520f0ca59756de4617534146595ab55027b706 | |
parent | 35a97e06cbeb8f6a018ddeea41c8a00e9b019a39 (diff) | |
parent | 69a9df1869729a82f3a8f7ebf2da4b58cf06cc4f (diff) | |
download | argh-08526c8a78b4e7b0374284635f8a9009c60c0f34.tar.gz |
Initial import. am: 0f3c1ab533 am: 68d187f38f am: 2b561b8c7a am: 69a9df1869
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/argh/+/1954545
Change-Id: I08219a26257ff41591233aad58589b26b2c1d906
-rw-r--r-- | .cargo_vcs_info.json | 6 | ||||
-rw-r--r-- | Android.bp | 61 | ||||
-rw-r--r-- | Cargo.toml | 26 | ||||
-rw-r--r-- | Cargo.toml.orig | 14 | ||||
-rw-r--r-- | LICENSE | 27 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_BSD_LIKE | 0 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rw-r--r-- | README.md | 177 | ||||
-rw-r--r-- | cargo2android.json | 9 | ||||
-rw-r--r-- | src/lib.rs | 993 | ||||
-rw-r--r-- | tests/lib.rs | 1308 |
12 files changed, 2641 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..23c489b --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "f1f85d2d89cbe09314dc1b59e581b8a43531cf3e" + }, + "path_in_vcs": "argh" +}
\ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..3b27556 --- /dev/null +++ b/Android.bp @@ -0,0 +1,61 @@ +// This file is generated by cargo2android.py --config cargo2android.json. +// Do not modify this file as changes will be overridden on upgrade. + + + +rust_test { + name: "argh_test_src_lib", + host_supported: true, + crate_name: "argh", + cargo_env_compat: true, + cargo_pkg_version: "0.1.7", + srcs: ["src/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", + rustlibs: [ + "libargh_shared", + ], + proc_macros: ["libargh_derive"], +} + +rust_test { + name: "argh_test_tests_lib", + host_supported: true, + crate_name: "lib", + cargo_env_compat: true, + cargo_pkg_version: "0.1.7", + srcs: ["tests/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", + rustlibs: [ + "libargh", + "libargh_shared", + ], + proc_macros: ["libargh_derive"], +} + +rust_library { + name: "libargh", + host_supported: true, + crate_name: "argh", + cargo_env_compat: true, + cargo_pkg_version: "0.1.7", + srcs: ["src/lib.rs"], + edition: "2018", + rustlibs: [ + "libargh_shared", + ], + proc_macros: ["libargh_derive"], + apex_available: [ + "//apex_available:platform", + "com.android.virt", + ], +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b20c0fc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "argh" +version = "0.1.7" +authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"] +description = "Derive-based argument parser optimized for code size" +readme = "README.md" +keywords = ["args", "arguments", "derive", "cli"] +license = "BSD-3-Clause" +repository = "https://github.com/google/argh" +[dependencies.argh_derive] +version = "0.1.7" + +[dependencies.argh_shared] +version = "0.1.7" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..ec92149 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,14 @@ +[package] +name = "argh" +version = "0.1.7" +authors = ["Taylor Cramer <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"] +edition = "2018" +keywords = ["args", "arguments", "derive", "cli"] +license = "BSD-3-Clause" +description = "Derive-based argument parser optimized for code size" +repository = "https://github.com/google/argh" +readme = "README.md" + +[dependencies] +argh_shared = { version = "0.1.7", path = "../argh_shared" } +argh_derive = { version = "0.1.7", path = "../argh_derive" } @@ -0,0 +1,27 @@ +Copyright 2019 The Fuchsia Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..a9d08d0 --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "argh" +description: "Derive-based argument parser optimized for code size" +third_party { + url { + type: HOMEPAGE + value: "https://crates.io/crates/argh" + } + url { + type: ARCHIVE + value: "https://static.crates.io/crates/argh/argh-0.1.7.crate" + } + version: "0.1.7" + license_type: NOTICE + last_upgrade_date { + year: 2022 + month: 1 + day: 13 + } +} diff --git a/MODULE_LICENSE_BSD_LIKE b/MODULE_LICENSE_BSD_LIKE new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_BSD_LIKE @@ -0,0 +1 @@ +include platform/prebuilts/rust:master:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e949e4 --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# Argh +**Argh is an opinionated Derive-based argument parser optimized for code size** + +[![crates.io](https://img.shields.io/crates/v/argh.svg)](https://crates.io/crates/argh) +[![license](https://img.shields.io/badge/license-BSD3.0-blue.svg)](https://github.com/google/argh/LICENSE) +[![docs.rs](https://docs.rs/argh/badge.svg)](https://docs.rs/crate/argh/) +![Argh](https://github.com/google/argh/workflows/Argh/badge.svg) + +Derive-based argument parsing optimized for code size and conformance +to the Fuchsia commandline tools specification + +The public API of this library consists primarily of the `FromArgs` +derive and the `from_env` function, which can be used to produce +a top-level `FromArgs` type from the current program's commandline +arguments. + +## Basic Example + +```rust,no_run +use argh::FromArgs; + +#[derive(FromArgs)] +/// Reach new heights. +struct GoUp { + /// whether or not to jump + #[argh(switch, short = 'j')] + jump: bool, + + /// how high to go + #[argh(option)] + height: usize, + + /// an optional nickname for the pilot + #[argh(option)] + pilot_nickname: Option<String>, +} + +fn main() { + let up: GoUp = argh::from_env(); +} +``` + +`./some_bin --help` will then output the following: + +``` +Usage: cmdname [-j] --height <height> [--pilot-nickname <pilot-nickname>] + +Reach new heights. + +Options: + -j, --jump whether or not to jump + --height how high to go + --pilot-nickname an optional nickname for the pilot + --help display usage information +``` + +The resulting program can then be used in any of these ways: +- `./some_bin --height 5` +- `./some_bin -j --height 5` +- `./some_bin --jump --height 5 --pilot-nickname Wes` + +Switches, like `jump`, are optional and will be set to true if provided. + +Options, like `height` and `pilot_nickname`, can be either required, +optional, or repeating, depending on whether they are contained in an +`Option` or a `Vec`. Default values can be provided using the +`#[argh(default = "<your_code_here>")]` attribute, and in this case an +option is treated as optional. + +```rust +use argh::FromArgs; + +fn default_height() -> usize { + 5 +} + +#[derive(FromArgs)] +/// Reach new heights. +struct GoUp { + /// an optional nickname for the pilot + #[argh(option)] + pilot_nickname: Option<String>, + + /// an optional height + #[argh(option, default = "default_height()")] + height: usize, + + /// an optional direction which is "up" by default + #[argh(option, default = "String::from(\"only up\")")] + direction: String, +} + +fn main() { + let up: GoUp = argh::from_env(); +} +``` + +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 +`fn(&str) -> Result<T, String>` using the `from_str_fn` attribute: + +```rust +use argh::FromArgs; + +#[derive(FromArgs)] +/// Goofy thing. +struct FiveStruct { + /// always five + #[argh(option, from_str_fn(always_five))] + five: usize, +} + +fn always_five(_value: &str) -> Result<usize, String> { + Ok(5) +} +``` + +Positional arguments can be declared using `#[argh(positional)]`. +These arguments will be parsed in order of their declaration in +the structure: + +```rust +use argh::FromArgs; + +#[derive(FromArgs, PartialEq, Debug)] +/// A command with positional arguments. +struct WithPositional { + #[argh(positional)] + first: String, +} +``` + +The last positional argument may include a default, or be wrapped in +`Option` or `Vec` to indicate an optional or repeating positional argument. + +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: + +```rust +use argh::FromArgs; + +#[derive(FromArgs, PartialEq, Debug)] +/// Top-level command. +struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), +} + +#[derive(FromArgs, PartialEq, Debug)] +/// First subcommand. +#[argh(subcommand, name = "one")] +struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, +} + +#[derive(FromArgs, PartialEq, Debug)] +/// Second subcommand. +#[argh(subcommand, name = "two")] +struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, +} +``` + +NOTE: This is not an officially supported Google product. diff --git a/cargo2android.json b/cargo2android.json new file mode 100644 index 0000000..6e516e0 --- /dev/null +++ b/cargo2android.json @@ -0,0 +1,9 @@ +{ + "apex-available": [ + "//apex_available:platform", + "com.android.virt" + ], + "device": true, + "run": true, + "tests": true +}
\ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..984d927 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,993 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//! Derive-based argument parsing optimized for code size and conformance +//! to the Fuchsia commandline tools specification +//! +//! The public API of this library consists primarily of the `FromArgs` +//! derive and the `from_env` function, which can be used to produce +//! a top-level `FromArgs` type from the current program's commandline +//! arguments. +//! +//! ## Basic Example +//! +//! ```rust,no_run +//! use argh::FromArgs; +//! +//! #[derive(FromArgs)] +//! /// Reach new heights. +//! struct GoUp { +//! /// whether or not to jump +//! #[argh(switch, short = 'j')] +//! jump: bool, +//! +//! /// how high to go +//! #[argh(option)] +//! height: usize, +//! +//! /// an optional nickname for the pilot +//! #[argh(option)] +//! pilot_nickname: Option<String>, +//! } +//! +//! fn main() { +//! let up: GoUp = argh::from_env(); +//! } +//! ``` +//! +//! `./some_bin --help` will then output the following: +//! +//! ```bash +//! Usage: cmdname [-j] --height <height> [--pilot-nickname <pilot-nickname>] +//! +//! Reach new heights. +//! +//! Options: +//! -j, --jump whether or not to jump +//! --height how high to go +//! --pilot-nickname an optional nickname for the pilot +//! --help display usage information +//! ``` +//! +//! The resulting program can then be used in any of these ways: +//! - `./some_bin --height 5` +//! - `./some_bin -j --height 5` +//! - `./some_bin --jump --height 5 --pilot-nickname Wes` +//! +//! Switches, like `jump`, are optional and will be set to true if provided. +//! +//! Options, like `height` and `pilot_nickname`, can be either required, +//! optional, or repeating, depending on whether they are contained in an +//! `Option` or a `Vec`. Default values can be provided using the +//! `#[argh(default = "<your_code_here>")]` attribute, and in this case an +//! option is treated as optional. +//! +//! ```rust +//! use argh::FromArgs; +//! +//! fn default_height() -> usize { +//! 5 +//! } +//! +//! #[derive(FromArgs)] +//! /// Reach new heights. +//! struct GoUp { +//! /// an optional nickname for the pilot +//! #[argh(option)] +//! pilot_nickname: Option<String>, +//! +//! /// an optional height +//! #[argh(option, default = "default_height()")] +//! height: usize, +//! +//! /// an optional direction which is "up" by default +//! #[argh(option, default = "String::from(\"only up\")")] +//! direction: String, +//! } +//! +//! fn main() { +//! let up: GoUp = argh::from_env(); +//! } +//! ``` +//! +//! 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 +//! `fn(&str) -> Result<T, String>` using the `from_str_fn` attribute: +//! +//! ``` +//! # use argh::FromArgs; +//! +//! #[derive(FromArgs)] +//! /// Goofy thing. +//! struct FiveStruct { +//! /// always five +//! #[argh(option, from_str_fn(always_five))] +//! five: usize, +//! } +//! +//! fn always_five(_value: &str) -> Result<usize, String> { +//! Ok(5) +//! } +//! ``` +//! +//! Positional arguments can be declared using `#[argh(positional)]`. +//! These arguments will be parsed in order of their declaration in +//! the structure: +//! +//! ```rust +//! use argh::FromArgs; +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// A command with positional arguments. +//! struct WithPositional { +//! #[argh(positional)] +//! first: String, +//! } +//! ``` +//! +//! The last positional argument may include a default, or be wrapped in +//! `Option` or `Vec` to indicate an optional or repeating positional argument. +//! +//! 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: +//! +//! ```rust +//! # use argh::FromArgs; +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// Top-level command. +//! struct TopLevel { +//! #[argh(subcommand)] +//! nested: MySubCommandEnum, +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! #[argh(subcommand)] +//! enum MySubCommandEnum { +//! One(SubCommandOne), +//! Two(SubCommandTwo), +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// First subcommand. +//! #[argh(subcommand, name = "one")] +//! struct SubCommandOne { +//! #[argh(option)] +//! /// how many x +//! x: usize, +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// Second subcommand. +//! #[argh(subcommand, name = "two")] +//! struct SubCommandTwo { +//! #[argh(switch)] +//! /// whether to fooey +//! fooey: bool, +//! } +//! ``` + +#![deny(missing_docs)] + +use std::str::FromStr; + +pub use argh_derive::FromArgs; + +/// Information about a particular command used for output. +pub type CommandInfo = argh_shared::CommandInfo<'static>; + +/// Types which can be constructed from a set of commandline arguments. +pub trait FromArgs: Sized { + /// Construct the type from an input set of arguments. + /// + /// The first argument `command_name` is the identifier for the current command. In most cases, + /// users should only pass in a single item for the command name, which typically comes from + /// the first item from `std::env::args()`. Implementations however should append the + /// subcommand name in when recursively calling [FromArgs::from_args] for subcommands. This + /// allows `argh` to generate correct subcommand help strings. + /// + /// The second argument `args` is the rest of the command line arguments. + /// + /// # Examples + /// + /// ```rust + /// # use argh::FromArgs; + /// + /// /// Command to manage a classroom. + /// #[derive(Debug, PartialEq, FromArgs)] + /// struct ClassroomCmd { + /// #[argh(subcommand)] + /// subcommands: Subcommands, + /// } + /// + /// #[derive(Debug, PartialEq, FromArgs)] + /// #[argh(subcommand)] + /// enum Subcommands { + /// List(ListCmd), + /// Add(AddCmd), + /// } + /// + /// /// list all the classes. + /// #[derive(Debug, PartialEq, FromArgs)] + /// #[argh(subcommand, name = "list")] + /// struct ListCmd { + /// /// list classes for only this teacher. + /// #[argh(option)] + /// teacher_name: Option<String>, + /// } + /// + /// /// add students to a class. + /// #[derive(Debug, PartialEq, FromArgs)] + /// #[argh(subcommand, name = "add")] + /// struct AddCmd { + /// /// the name of the class's teacher. + /// #[argh(option)] + /// teacher_name: String, + /// + /// /// the name of the class. + /// #[argh(positional)] + /// class_name: String, + /// } + /// + /// let args = ClassroomCmd::from_args( + /// &["classroom"], + /// &["list", "--teacher-name", "Smith"], + /// ).unwrap(); + /// assert_eq!( + /// args, + /// ClassroomCmd { + /// subcommands: Subcommands::List(ListCmd { + /// teacher_name: Some("Smith".to_string()), + /// }) + /// }, + /// ); + /// + /// // Help returns an error, but internally returns an `Ok` status. + /// let early_exit = ClassroomCmd::from_args( + /// &["classroom"], + /// &["help"], + /// ).unwrap_err(); + /// assert_eq!( + /// early_exit, + /// argh::EarlyExit { + /// output: r#"Usage: classroom <command> [<args>] + /// + /// Command to manage a classroom. + /// + /// Options: + /// --help display usage information + /// + /// Commands: + /// list list all the classes. + /// add add students to a class. + /// "#.to_string(), + /// status: Ok(()), + /// }, + /// ); + /// + /// // Help works with subcommands. + /// let early_exit = ClassroomCmd::from_args( + /// &["classroom"], + /// &["list", "help"], + /// ).unwrap_err(); + /// assert_eq!( + /// early_exit, + /// argh::EarlyExit { + /// output: r#"Usage: classroom list [--teacher-name <teacher-name>] + /// + /// list all the classes. + /// + /// Options: + /// --teacher-name list classes for only this teacher. + /// --help display usage information + /// "#.to_string(), + /// status: Ok(()), + /// }, + /// ); + /// + /// // Incorrect arguments will error out. + /// let err = ClassroomCmd::from_args( + /// &["classroom"], + /// &["lisp"], + /// ).unwrap_err(); + /// assert_eq!( + /// err, + /// argh::EarlyExit { + /// output: "Unrecognized argument: lisp\n".to_string(), + /// status: Err(()), + /// }, + /// ); + /// ``` + fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, EarlyExit>; + + /// Get a String with just the argument names, e.g., options, flags, subcommands, etc, but + /// without the values of the options and arguments. This can be useful as a means to capture + /// anonymous usage statistics without revealing the content entered by the end user. + /// + /// The first argument `command_name` is the identifier for the current command. In most cases, + /// users should only pass in a single item for the command name, which typically comes from + /// the first item from `std::env::args()`. Implementations however should append the + /// subcommand name in when recursively calling [FromArgs::from_args] for subcommands. This + /// allows `argh` to generate correct subcommand help strings. + /// + /// The second argument `args` is the rest of the command line arguments. + /// + /// # Examples + /// + /// ```rust + /// # use argh::FromArgs; + /// + /// /// Command to manage a classroom. + /// #[derive(FromArgs)] + /// struct ClassroomCmd { + /// #[argh(subcommand)] + /// subcommands: Subcommands, + /// } + /// + /// #[derive(FromArgs)] + /// #[argh(subcommand)] + /// enum Subcommands { + /// List(ListCmd), + /// Add(AddCmd), + /// } + /// + /// /// list all the classes. + /// #[derive(FromArgs)] + /// #[argh(subcommand, name = "list")] + /// struct ListCmd { + /// /// list classes for only this teacher. + /// #[argh(option)] + /// teacher_name: Option<String>, + /// } + /// + /// /// add students to a class. + /// #[derive(FromArgs)] + /// #[argh(subcommand, name = "add")] + /// struct AddCmd { + /// /// the name of the class's teacher. + /// #[argh(option)] + /// teacher_name: String, + /// + /// /// has the class started yet? + /// #[argh(switch)] + /// started: bool, + /// + /// /// the name of the class. + /// #[argh(positional)] + /// class_name: String, + /// + /// /// the student names. + /// #[argh(positional)] + /// students: Vec<String>, + /// } + /// + /// let args = ClassroomCmd::redact_arg_values( + /// &["classroom"], + /// &["list"], + /// ).unwrap(); + /// assert_eq!( + /// args, + /// &[ + /// "classroom", + /// "list", + /// ], + /// ); + /// + /// let args = ClassroomCmd::redact_arg_values( + /// &["classroom"], + /// &["list", "--teacher-name", "Smith"], + /// ).unwrap(); + /// assert_eq!( + /// args, + /// &[ + /// "classroom", + /// "list", + /// "--teacher-name", + /// ], + /// ); + /// + /// let args = ClassroomCmd::redact_arg_values( + /// &["classroom"], + /// &["add", "--teacher-name", "Smith", "--started", "Math", "Abe", "Sung"], + /// ).unwrap(); + /// assert_eq!( + /// args, + /// &[ + /// "classroom", + /// "add", + /// "--teacher-name", + /// "--started", + /// "class_name", + /// "students", + /// "students", + /// ], + /// ); + /// + /// // `ClassroomCmd::redact_arg_values` will error out if passed invalid arguments. + /// assert_eq!( + /// ClassroomCmd::redact_arg_values(&["classroom"], &["add", "--teacher-name"]), + /// Err(argh::EarlyExit { + /// output: "No value provided for option '--teacher-name'.\n".into(), + /// status: Err(()), + /// }), + /// ); + /// + /// // `ClassroomCmd::redact_arg_values` will generate help messages. + /// assert_eq!( + /// ClassroomCmd::redact_arg_values(&["classroom"], &["help"]), + /// Err(argh::EarlyExit { + /// output: r#"Usage: classroom <command> [<args>] + /// + /// Command to manage a classroom. + /// + /// Options: + /// --help display usage information + /// + /// Commands: + /// list list all the classes. + /// add add students to a class. + /// "#.to_string(), + /// status: Ok(()), + /// }), + /// ); + /// ``` + fn redact_arg_values(_command_name: &[&str], _args: &[&str]) -> Result<Vec<String>, EarlyExit> { + Ok(vec!["<<REDACTED>>".into()]) + } +} + +/// A top-level `FromArgs` implementation that is not a subcommand. +pub trait TopLevelCommand: FromArgs {} + +/// A `FromArgs` implementation that can parse into one or more subcommands. +pub trait SubCommands: FromArgs { + /// Info for the commands. + const COMMANDS: &'static [&'static CommandInfo]; +} + +/// A `FromArgs` implementation that represents a single subcommand. +pub trait SubCommand: FromArgs { + /// Information about the subcommand. + const COMMAND: &'static CommandInfo; +} + +impl<T: SubCommand> SubCommands for T { + const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND]; +} + +/// Information to display to the user about why a `FromArgs` construction exited early. +/// +/// This can occur due to either failed parsing or a flag like `--help`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EarlyExit { + /// The output to display to the user of the commandline tool. + pub output: String, + /// Status of argument parsing. + /// + /// `Ok` if the command was parsed successfully and the early exit is due + /// to a flag like `--help` causing early exit with output. + /// + /// `Err` if the arguments were not successfully parsed. + // TODO replace with std::process::ExitCode when stable. + pub status: Result<(), ()>, +} + +impl From<String> for EarlyExit { + fn from(err_msg: String) -> Self { + Self { output: err_msg, status: Err(()) } + } +} + +/// Extract the base cmd from a path +fn cmd<'a>(default: &'a String, path: &'a String) -> &'a str { + std::path::Path::new(path).file_name().map(|s| s.to_str()).flatten().unwrap_or(default.as_str()) +} + +/// Create a `FromArgs` type from the current process's `env::args`. +/// +/// This function will exit early from the current process if argument parsing +/// was unsuccessful or if information like `--help` was requested. Error messages will be printed +/// to stderr, and `--help` output to stdout. +pub fn from_env<T: TopLevelCommand>() -> T { + let strings: Vec<String> = std::env::args().collect(); + let cmd = cmd(&strings[0], &strings[0]); + let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); + T::from_args(&[cmd], &strs[1..]).unwrap_or_else(|early_exit| { + std::process::exit(match early_exit.status { + Ok(()) => { + println!("{}", early_exit.output); + 0 + } + Err(()) => { + eprintln!("{}", early_exit.output); + 1 + } + }) + }) +} + +/// Create a `FromArgs` type from the current process's `env::args`. +/// +/// This special cases usages where argh is being used in an environment where cargo is +/// driving the build. We skip the second env variable. +/// +/// This function will exit early from the current process if argument parsing +/// was unsuccessful or if information like `--help` was requested. Error messages will be printed +/// to stderr, and `--help` output to stdout. +pub fn cargo_from_env<T: TopLevelCommand>() -> T { + let strings: Vec<String> = std::env::args().collect(); + let cmd = cmd(&strings[1], &strings[1]); + let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); + T::from_args(&[cmd], &strs[2..]).unwrap_or_else(|early_exit| { + std::process::exit(match early_exit.status { + Ok(()) => { + println!("{}", early_exit.output); + 0 + } + Err(()) => { + eprintln!("{}", early_exit.output); + 1 + } + }) + }) +} + +/// Types which can be constructed from a single commandline value. +/// +/// Any field type declared in a struct that derives `FromArgs` must implement +/// this trait. A blanket implementation exists for types implementing +/// `FromStr<Error: Display>`. Custom types can implement this trait +/// directly. +pub trait FromArgValue: Sized { + /// Construct the type from a commandline value, returning an error string + /// on failure. + fn from_arg_value(value: &str) -> Result<Self, String>; +} + +impl<T> FromArgValue for T +where + T: FromStr, + T::Err: std::fmt::Display, +{ + fn from_arg_value(value: &str) -> Result<Self, String> { + T::from_str(value).map_err(|x| x.to_string()) + } +} + +// The following items are all used by the generated code, and should not be considered part +// of this library's public API surface. + +#[doc(hidden)] +pub trait ParseFlag { + fn set_flag(&mut self, arg: &str); +} + +impl<T: Flag> ParseFlag for T { + fn set_flag(&mut self, _arg: &str) { + <T as Flag>::set_flag(self); + } +} + +#[doc(hidden)] +pub struct RedactFlag { + pub slot: Option<String>, +} + +impl ParseFlag for RedactFlag { + fn set_flag(&mut self, arg: &str) { + self.slot = Some(arg.to_string()); + } +} + +// A trait for for slots that reserve space for a value and know how to parse that value +// from a command-line `&str` argument. +// +// This trait is only implemented for the type `ParseValueSlotTy`. This indirection is +// necessary to allow abstracting over `ParseValueSlotTy` instances with different +// generic parameters. +#[doc(hidden)] +pub trait ParseValueSlot { + fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String>; +} + +// The concrete type implementing the `ParseValueSlot` trait. +// +// `T` is the type to be parsed from a single string. +// `Slot` is the type of the container that can hold a value or values of type `T`. +#[doc(hidden)] +pub struct ParseValueSlotTy<Slot, T> { + // The slot for a parsed value. + pub slot: Slot, + // The function to parse the value from a string + pub parse_func: fn(&str, &str) -> Result<T, String>, +} + +// `ParseValueSlotTy<Option<T>, T>` is used as the slot for all non-repeating +// arguments, both optional and required. +impl<T> ParseValueSlot for ParseValueSlotTy<Option<T>, T> { + fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String> { + if self.slot.is_some() { + return Err("duplicate values provided".to_string()); + } + self.slot = Some((self.parse_func)(arg, value)?); + Ok(()) + } +} + +// `ParseValueSlotTy<Vec<T>, T>` is used as the slot for repeating arguments. +impl<T> ParseValueSlot for ParseValueSlotTy<Vec<T>, T> { + fn fill_slot(&mut self, arg: &str, value: &str) -> Result<(), String> { + self.slot.push((self.parse_func)(arg, value)?); + Ok(()) + } +} + +/// A type which can be the receiver of a `Flag`. +pub trait Flag { + /// Creates a default instance of the flag value; + fn default() -> Self + where + Self: Sized; + + /// Sets the flag. This function is called when the flag is provided. + fn set_flag(&mut self); +} + +impl Flag for bool { + fn default() -> Self { + false + } + fn set_flag(&mut self) { + *self = true; + } +} + +macro_rules! impl_flag_for_integers { + ($($ty:ty,)*) => { + $( + impl Flag for $ty { + fn default() -> Self { + 0 + } + fn set_flag(&mut self) { + *self = self.saturating_add(1); + } + } + )* + } +} + +impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,]; + +/// This function implements argument parsing for structs. +/// +/// `cmd_name`: The identifier for the current command. +/// `args`: The command line arguments. +/// `parse_options`: Helper to parse optional arguments. +/// `parse_positionals`: Helper to parse positional arguments. +/// `parse_subcommand`: Helper to parse a subcommand. +/// `help_func`: Generate a help message. +#[doc(hidden)] +pub fn parse_struct_args( + cmd_name: &[&str], + args: &[&str], + mut parse_options: ParseStructOptions<'_>, + mut parse_positionals: ParseStructPositionals<'_>, + mut parse_subcommand: Option<ParseStructSubCommand<'_>>, + help_func: &dyn Fn() -> String, +) -> Result<(), EarlyExit> { + let mut help = false; + let mut remaining_args = args; + let mut positional_index = 0; + let mut options_ended = false; + + 'parse_args: while let Some(&next_arg) = remaining_args.get(0) { + remaining_args = &remaining_args[1..]; + if (next_arg == "--help" || next_arg == "help") && !options_ended { + help = true; + continue; + } + + if next_arg.starts_with("-") && !options_ended { + if next_arg == "--" { + options_ended = true; + continue; + } + + if help { + return Err("Trailing arguments are not allowed after `help`.".to_string().into()); + } + + parse_options.parse(next_arg, &mut remaining_args)?; + continue; + } + + if let Some(ref mut parse_subcommand) = parse_subcommand { + if parse_subcommand.parse(help, cmd_name, next_arg, remaining_args)? { + // Unset `help`, since we handled it in the subcommand + help = false; + break 'parse_args; + } + } + + parse_positionals.parse(&mut positional_index, next_arg)?; + } + + if help { + Err(EarlyExit { output: help_func(), status: Ok(()) }) + } else { + Ok(()) + } +} + +#[doc(hidden)] +pub struct ParseStructOptions<'a> { + /// A mapping from option string literals to the entry + /// in the output table. This may contain multiple entries mapping to + /// the same location in the table if both a short and long version + /// of the option exist (`-z` and `--zoo`). + pub arg_to_slot: &'static [(&'static str, usize)], + + /// The storage for argument output data. + pub slots: &'a mut [ParseStructOption<'a>], +} + +impl<'a> ParseStructOptions<'a> { + /// Parse a commandline option. + /// + /// `arg`: the current option argument being parsed (e.g. `--foo`). + /// `remaining_args`: the remaining command line arguments. This slice + /// will be advanced forwards if the option takes a value argument. + fn parse(&mut self, arg: &str, remaining_args: &mut &[&str]) -> Result<(), String> { + let pos = self + .arg_to_slot + .iter() + .find_map(|&(name, pos)| if name == arg { Some(pos) } else { None }) + .ok_or_else(|| unrecognized_argument(arg))?; + + match self.slots[pos] { + ParseStructOption::Flag(ref mut b) => b.set_flag(arg), + ParseStructOption::Value(ref mut pvs) => { + let value = remaining_args + .get(0) + .ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?; + *remaining_args = &remaining_args[1..]; + pvs.fill_slot(arg, value).map_err(|s| { + ["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"] + .concat() + })?; + } + } + + Ok(()) + } +} + +fn unrecognized_argument(x: &str) -> String { + ["Unrecognized argument: ", x, "\n"].concat() +} + +// `--` or `-` options, including a mutable reference to their value. +#[doc(hidden)] +pub enum ParseStructOption<'a> { + // A flag which is set to `true` when provided. + Flag(&'a mut dyn ParseFlag), + // A value which is parsed from the string following the `--` argument, + // e.g. `--foo bar`. + Value(&'a mut dyn ParseValueSlot), +} + +#[doc(hidden)] +pub struct ParseStructPositionals<'a> { + pub positionals: &'a mut [ParseStructPositional<'a>], + pub last_is_repeating: bool, +} + +impl<'a> ParseStructPositionals<'a> { + /// Parse the next positional argument. + /// + /// `arg`: the argument supplied by the user. + fn parse(&mut self, index: &mut usize, arg: &str) -> Result<(), EarlyExit> { + if *index < self.positionals.len() { + self.positionals[*index].parse(arg)?; + + // Don't increment position if we're at the last arg + // *and* the last arg is repeating. + let skip_increment = self.last_is_repeating && *index == self.positionals.len() - 1; + + if !skip_increment { + *index += 1; + } + + Ok(()) + } else { + Err(EarlyExit { output: unrecognized_arg(arg), status: Err(()) }) + } + } +} + +#[doc(hidden)] +pub struct ParseStructPositional<'a> { + // The positional's name + pub name: &'static str, + + // The function to parse the positional. + pub slot: &'a mut dyn ParseValueSlot, +} + +impl<'a> ParseStructPositional<'a> { + /// Parse a positional argument. + /// + /// `arg`: the argument supplied by the user. + fn parse(&mut self, arg: &str) -> Result<(), EarlyExit> { + self.slot.fill_slot("", arg).map_err(|s| { + [ + "Error parsing positional argument '", + self.name, + "' with value '", + arg, + "': ", + &s, + "\n", + ] + .concat() + .into() + }) + } +} + +// A type to simplify parsing struct subcommands. +// +// This indirection is necessary to allow abstracting over `FromArgs` instances with different +// generic parameters. +#[doc(hidden)] +pub struct ParseStructSubCommand<'a> { + // The subcommand commands + pub subcommands: &'static [&'static CommandInfo], + + // The function to parse the subcommand arguments. + pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>, +} + +impl<'a> ParseStructSubCommand<'a> { + fn parse( + &mut self, + help: bool, + cmd_name: &[&str], + arg: &str, + remaining_args: &[&str], + ) -> Result<bool, EarlyExit> { + for subcommand in self.subcommands { + if subcommand.name == arg { + let mut command = cmd_name.to_owned(); + command.push(subcommand.name); + let prepended_help; + let remaining_args = if help { + prepended_help = prepend_help(remaining_args); + &prepended_help + } else { + remaining_args + }; + + (self.parse_func)(&command, remaining_args)?; + + return Ok(true); + } + } + + return Ok(false); + } +} + +// Prepend `help` to a list of arguments. +// This is used to pass the `help` argument on to subcommands. +fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> { + [&["help"], args].concat() +} + +#[doc(hidden)] +pub fn print_subcommands(commands: &[&CommandInfo]) -> String { + let mut out = String::new(); + for cmd in commands { + argh_shared::write_description(&mut out, cmd); + } + out +} + +fn unrecognized_arg(arg: &str) -> String { + ["Unrecognized argument: ", arg, "\n"].concat() +} + +// An error string builder to report missing required options and subcommands. +#[doc(hidden)] +#[derive(Default)] +pub struct MissingRequirements { + options: Vec<&'static str>, + subcommands: Option<&'static [&'static CommandInfo]>, + positional_args: Vec<&'static str>, +} + +const NEWLINE_INDENT: &str = "\n "; + +impl MissingRequirements { + // Add a missing required option. + #[doc(hidden)] + pub fn missing_option(&mut self, name: &'static str) { + self.options.push(name) + } + + // Add a missing required subcommand. + #[doc(hidden)] + pub fn missing_subcommands(&mut self, commands: &'static [&'static CommandInfo]) { + self.subcommands = Some(commands); + } + + // Add a missing positional argument. + #[doc(hidden)] + pub fn missing_positional_arg(&mut self, name: &'static str) { + self.positional_args.push(name) + } + + // If any missing options or subcommands were provided, returns an error string + // describing the missing args. + #[doc(hidden)] + pub fn err_on_any(&self) -> Result<(), String> { + if self.options.is_empty() && self.subcommands.is_none() && self.positional_args.is_empty() + { + return Ok(()); + } + + let mut output = String::new(); + + if !self.positional_args.is_empty() { + output.push_str("Required positional arguments not provided:"); + for arg in &self.positional_args { + output.push_str(NEWLINE_INDENT); + output.push_str(arg); + } + } + + if !self.options.is_empty() { + if !self.positional_args.is_empty() { + output.push_str("\n"); + } + output.push_str("Required options not provided:"); + for option in &self.options { + output.push_str(NEWLINE_INDENT); + output.push_str(option); + } + } + + if let Some(missing_subcommands) = self.subcommands { + if !self.options.is_empty() { + output.push_str("\n"); + } + output.push_str("One of the following subcommands must be present:"); + output.push_str(NEWLINE_INDENT); + output.push_str("help"); + for subcommand in missing_subcommands { + output.push_str(NEWLINE_INDENT); + output.push_str(subcommand.name); + } + } + + output.push('\n'); + + Err(output) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_cmd_extraction() { + let expected = "test_cmd"; + let path = format!("/tmp/{}", expected); + let cmd = cmd(&path, &path); + assert_eq!(expected, cmd); + } +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..fe8c858 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,1308 @@ +#![cfg(test)] +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use {argh::FromArgs, std::fmt::Debug}; + +#[test] +fn basic_example() { + #[derive(FromArgs, PartialEq, Debug)] + /// Reach new heights. + struct GoUp { + /// whether or not to jump + #[argh(switch, short = 'j')] + jump: bool, + + /// how high to go + #[argh(option)] + height: usize, + + /// an optional nickname for the pilot + #[argh(option)] + pilot_nickname: Option<String>, + } + + let up = GoUp::from_args(&["cmdname"], &["--height", "5"]).expect("failed go_up"); + assert_eq!(up, GoUp { jump: false, height: 5, pilot_nickname: None }); +} + +#[test] +fn custom_from_str_example() { + #[derive(FromArgs)] + /// Goofy thing. + struct FiveStruct { + /// always five + #[argh(option, from_str_fn(always_five))] + five: usize, + } + + fn always_five(_value: &str) -> Result<usize, String> { + Ok(5) + } + + let f = FiveStruct::from_args(&["cmdname"], &["--five", "woot"]).expect("failed to five"); + assert_eq!(f.five, 5); +} + +#[test] +fn subcommand_example() { + #[derive(FromArgs, PartialEq, Debug)] + /// Top-level command. + struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + let one = TopLevel::from_args(&["cmdname"], &["one", "--x", "2"]).expect("sc 1"); + assert_eq!(one, TopLevel { nested: MySubCommandEnum::One(SubCommandOne { x: 2 }) },); + + let two = TopLevel::from_args(&["cmdname"], &["two", "--fooey"]).expect("sc 2"); + assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); +} + +#[test] +fn multiline_doc_comment_description() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(switch)] + /// a switch with a description + /// that is spread across + /// a number of + /// lines of comments. + _s: bool, + } + + assert_help_string::<Cmd>( + r###"Usage: test_arg_0 [--s] + +Short description + +Options: + --s a switch with a description that is spread across a number + of lines of comments. + --help display usage information +"###, + ); +} + +#[test] +fn explicit_long_value_for_option() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option, long = "foo")] + /// bar bar + x: u8, + } + + let cmd = Cmd::from_args(&["cmdname"], &["--foo", "5"]).unwrap(); + assert_eq!(cmd.x, 5); +} + +/// Test that descriptions can start with an initialism despite +/// usually being required to start with a lowercase letter. +#[derive(FromArgs)] +#[allow(unused)] +struct DescriptionStartsWithInitialism { + /// URL fooey + #[argh(option)] + x: u8, +} + +#[test] +fn default_number() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(option, default = "5")] + /// fooey + x: u8, + } + + let cmd = Cmd::from_args(&["cmdname"], &[]).unwrap(); + assert_eq!(cmd.x, 5); +} + +#[test] +fn default_function() { + const MSG: &str = "hey I just met you"; + fn call_me_maybe() -> String { + MSG.to_string() + } + + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(option, default = "call_me_maybe()")] + /// fooey + msg: String, + } + + let cmd = Cmd::from_args(&["cmdname"], &[]).unwrap(); + assert_eq!(cmd.msg, MSG); +} + +#[test] +fn missing_option_value() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// fooey + _msg: String, + } + + let e = Cmd::from_args(&["cmdname"], &["--msg"]) + .expect_err("Parsing missing option value should fail"); + assert_eq!(e.output, "No value provided for option \'--msg\'.\n"); + assert!(e.status.is_err()); +} + +fn assert_help_string<T: FromArgs>(help_str: &str) { + match T::from_args(&["test_arg_0"], &["--help"]) { + Ok(_) => panic!("help was parsed as args"), + Err(e) => { + assert_eq!(help_str, e.output); + e.status.expect("help returned an error"); + } + } +} + +fn assert_output<T: FromArgs + Debug + PartialEq>(args: &[&str], expected: T) { + let t = T::from_args(&["cmd"], args).expect("failed to parse"); + assert_eq!(t, expected); +} + +fn assert_error<T: FromArgs + Debug>(args: &[&str], err_msg: &str) { + let e = T::from_args(&["cmd"], args).expect_err("unexpectedly succeeded parsing"); + assert_eq!(err_msg, e.output); + e.status.expect_err("error had a positive status"); +} + +mod options { + use super::*; + + #[derive(argh::FromArgs, Debug, PartialEq)] + /// Woot + struct Parsed { + #[argh(option, short = 'n')] + /// fooey + n: usize, + } + + #[test] + fn parsed() { + assert_output(&["-n", "5"], Parsed { n: 5 }); + assert_error::<Parsed>( + &["-n", "x"], + r###"Error parsing option '-n' with value 'x': invalid digit found in string +"###, + ); + } + + #[derive(argh::FromArgs, Debug, PartialEq)] + /// Woot + struct Repeating { + #[argh(option, short = 'n')] + /// fooey + n: Vec<String>, + } + + #[test] + fn repeating() { + assert_help_string::<Repeating>( + r###"Usage: test_arg_0 [-n <n...>] + +Woot + +Options: + -n, --n fooey + --help display usage information +"###, + ); + } + + #[derive(argh::FromArgs, Debug, PartialEq)] + /// Woot + struct WithArgName { + #[argh(option, arg_name = "name")] + /// fooey + option_name: Option<String>, + } + + #[test] + fn with_arg_name() { + assert_help_string::<WithArgName>( + r###"Usage: test_arg_0 [--option-name <name>] + +Woot + +Options: + --option-name fooey + --help display usage information +"###, + ); + } +} + +mod positional { + use super::*; + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastRepeating { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional)] + /// fooey + b: Vec<String>, + } + + #[test] + fn repeating() { + assert_output(&["5"], LastRepeating { a: 5, b: vec![] }); + assert_output(&["5", "foo"], LastRepeating { a: 5, b: vec!["foo".into()] }); + assert_output( + &["5", "foo", "bar"], + LastRepeating { a: 5, b: vec!["foo".into(), "bar".into()] }, + ); + assert_help_string::<LastRepeating>( + r###"Usage: test_arg_0 <a> [<b...>] + +Woot + +Positional Arguments: + a fooey + b fooey + +Options: + --help display usage information +"###, + ); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastOptional { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional)] + /// fooey + b: Option<String>, + } + + #[test] + fn optional() { + assert_output(&["5"], LastOptional { a: 5, b: None }); + assert_output(&["5", "6"], LastOptional { a: 5, b: Some("6".into()) }); + assert_error::<LastOptional>(&["5", "6", "7"], "Unrecognized argument: 7\n"); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastDefaulted { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional, default = "5")] + /// fooey + b: u32, + } + + #[test] + fn defaulted() { + assert_output(&["5"], LastDefaulted { a: 5, b: 5 }); + assert_output(&["5", "6"], LastDefaulted { a: 5, b: 6 }); + assert_error::<LastDefaulted>(&["5", "6", "7"], "Unrecognized argument: 7\n"); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastRequired { + #[argh(positional)] + /// fooey + a: u32, + #[argh(positional)] + /// fooey + b: u32, + } + + #[test] + fn required() { + assert_output(&["5", "6"], LastRequired { a: 5, b: 6 }); + assert_error::<LastRequired>( + &[], + r###"Required positional arguments not provided: + a + b +"###, + ); + assert_error::<LastRequired>( + &["5"], + r###"Required positional arguments not provided: + b +"###, + ); + } + + #[derive(argh::FromArgs, Debug, PartialEq)] + /// Woot + struct Parsed { + #[argh(positional)] + /// fooey + n: usize, + } + + #[test] + fn parsed() { + assert_output(&["5"], Parsed { n: 5 }); + assert_error::<Parsed>( + &["x"], + r###"Error parsing positional argument 'n' with value 'x': invalid digit found in string +"###, + ); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct WithOption { + #[argh(positional)] + /// fooey + a: String, + #[argh(option)] + /// fooey + b: String, + } + + #[test] + fn mixed_with_option() { + assert_output(&["first", "--b", "foo"], WithOption { a: "first".into(), b: "foo".into() }); + + assert_error::<WithOption>( + &[], + r###"Required positional arguments not provided: + a +Required options not provided: + --b +"###, + ); + } + + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct WithSubcommand { + #[argh(positional)] + /// fooey + a: String, + #[argh(subcommand)] + /// fooey + b: Subcommand, + #[argh(positional)] + /// fooey + c: Vec<String>, + } + + #[derive(FromArgs, Debug, PartialEq)] + #[argh(subcommand, name = "a")] + /// Subcommand of positional::WithSubcommand. + struct Subcommand { + #[argh(positional)] + /// fooey + a: String, + #[argh(positional)] + /// fooey + b: Vec<String>, + } + + #[test] + fn mixed_with_subcommand() { + assert_output( + &["first", "a", "a"], + WithSubcommand { + a: "first".into(), + b: Subcommand { a: "a".into(), b: vec![] }, + c: vec![], + }, + ); + + assert_error::<WithSubcommand>( + &["a", "a", "a"], + r###"Required positional arguments not provided: + a +"###, + ); + + assert_output( + &["1", "2", "3", "a", "b", "c"], + WithSubcommand { + a: "1".into(), + b: Subcommand { a: "b".into(), b: vec!["c".into()] }, + c: vec!["2".into(), "3".into()], + }, + ); + } +} + +/// Tests derived from +/// https://fuchsia.dev/fuchsia-src/development/api/cli and +/// https://fuchsia.dev/fuchsia-src/development/api/cli_help +mod fuchsia_commandline_tools_rubric { + use super::*; + + /// Tests for the three required command line argument types: + /// - exact text + /// - arguments + /// - options (i.e. switches and keys) + #[test] + fn three_command_line_argument_types() { + // TODO(cramertj) add support for exact text and positional arguments + } + + /// A piece of exact text may be required or optional + #[test] + fn exact_text_required_and_optional() { + // TODO(cramertj) add support for exact text + } + + /// Arguments are like function parameters or slots for data. + /// The order often matters. + #[test] + fn arguments_ordered() { + // TODO(cramertj) add support for ordered positional arguments + } + + /// If a single argument is repeated, order may not matter, e.g. `<files>...` + #[test] + fn arguments_unordered() { + // TODO(cramertj) add support for repeated positional arguments + } + + // Short argument names must use one dash and a single letter. + // TODO(cramertj): this should be a compile-fail test + + // Short argument names are optional, but all choices are required to have a `--` option. + // TODO(cramertj): this should be a compile-fail test + + // Numeric options, such as `-1` and `-2`, are not allowed. + // TODO(cramertj): this should be a compile-fail test + + #[derive(FromArgs)] + /// One switch. + struct OneSwitch { + #[argh(switch, short = 's')] + /// just a switch + switchy: bool, + } + + /// The presence of a switch means the feature it represents is "on", + /// while its absence means that it is "off". + #[test] + fn switch_on_when_present() { + let on = OneSwitch::from_args(&["cmdname"], &["-s"]).expect("parsing on"); + assert!(on.switchy); + + let off = OneSwitch::from_args(&["cmdname"], &[]).expect("parsing off"); + assert!(!off.switchy); + } + + #[derive(FromArgs, Debug)] + /// Two Switches + struct TwoSwitches { + #[argh(switch, short = 'a')] + /// a + _a: bool, + #[argh(switch, short = 'b')] + /// b + _b: bool, + } + + /// Running switches together is not allowed + #[test] + fn switches_cannot_run_together() { + TwoSwitches::from_args(&["cmdname"], &["-a", "-b"]) + .expect("parsing separate should succeed"); + TwoSwitches::from_args(&["cmdname"], &["-ab"]).expect_err("parsing together should fail"); + } + + #[derive(FromArgs, Debug)] + /// One keyed option + struct OneOption { + #[argh(option)] + /// some description + _foo: String, + } + + /// Do not use an equals punctuation or similar to separate the key and value. + #[test] + fn keyed_no_equals() { + OneOption::from_args(&["cmdname"], &["--foo", "bar"]) + .expect("Parsing option value as separate arg should succeed"); + + let e = OneOption::from_args(&["cmdname"], &["--foo=bar"]) + .expect_err("Parsing option value using `=` should fail"); + assert_eq!(e.output, "Unrecognized argument: --foo=bar\n"); + assert!(e.status.is_err()); + } + + // Two dashes on their own indicates the end of options. + // Subsequent values are given to the tool as-is. + // + // It's unclear exactly what "are given to the tool as-is" in means in this + // context, so we provide a few options for handling `--`, with it being + // an error by default. + // + // TODO(cramertj) implement some behavior for `--` + + /// Double-dash is treated as an error by default. + #[test] + fn double_dash_default_error() {} + + /// Double-dash can be ignored for later manual parsing. + #[test] + fn double_dash_ignore() {} + + /// Double-dash should be treated as the end of flags and optional arguments, + /// and the remainder of the values should be treated purely as positional arguments, + /// even when their syntax matches that of options. e.g. `foo -- -e` should be parsed + /// as passing a single positional argument with the value `-e`. + #[test] + fn double_dash_positional() { + #[derive(FromArgs, Debug, PartialEq)] + /// Positional arguments list + struct StringList { + #[argh(positional)] + /// a list of strings + strs: Vec<String>, + + #[argh(switch)] + /// some flag + flag: bool, + } + + assert_output( + &["--", "a", "-b", "--flag"], + StringList { strs: vec!["a".into(), "-b".into(), "--flag".into()], flag: false }, + ); + assert_output( + &["--flag", "--", "-a", "b"], + StringList { strs: vec!["-a".into(), "b".into()], flag: true }, + ); + assert_output(&["--", "--help"], StringList { strs: vec!["--help".into()], flag: false }); + assert_output( + &["--", "-a", "--help"], + StringList { strs: vec!["-a".into(), "--help".into()], flag: false }, + ); + } + + /// Double-dash can be parsed into an optional field using a provided + /// `fn(&[&str]) -> Result<T, EarlyExit>`. + #[test] + fn double_dash_custom() {} + + /// Repeating switches may be used to apply more emphasis. + /// A common example is increasing verbosity by passing more `-v` switches. + #[test] + fn switches_repeating() { + #[derive(FromArgs, Debug)] + /// A type for testing repeating `-v` + struct CountVerbose { + #[argh(switch, short = 'v')] + /// increase the verbosity of the command. + verbose: i128, + } + + let cv = CountVerbose::from_args(&["cmdname"], &["-v", "-v", "-v"]) + .expect("Parsing verbose flags should succeed"); + assert_eq!(cv.verbose, 3); + } + + // When a tool has many subcommands, it should also have a help subcommand + // that displays help about the subcommands, e.g. `fx help build`. + // + // Elsewhere in the docs, it says the syntax `--help` is required, so we + // interpret that to mean: + // + // - `help` should always be accepted as a "keyword" in place of the first + // positional argument for both the main command and subcommands. + // + // - If followed by the name of a subcommand it should forward to the + // `--help` of said subcommand, otherwise it will fall back to the + // help of the righmost command / subcommand. + // + // - `--help` will always consider itself the only meaningful argument to + // the rightmost command / subcommand, and any following arguments will + // be treated as an error. + + #[derive(FromArgs, Debug)] + /// A type for testing `--help`/`help` + struct HelpTopLevel { + #[argh(subcommand)] + _sub: HelpFirstSub, + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "first")] + /// First subcommmand for testing `help`. + struct HelpFirstSub { + #[argh(subcommand)] + _sub: HelpSecondSub, + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "second")] + /// Second subcommand for testing `help`. + struct HelpSecondSub {} + + fn expect_help(args: &[&str], expected_help_string: &str) { + let e = HelpTopLevel::from_args(&["cmdname"], args).expect_err("should exit early"); + assert_eq!(expected_help_string, e.output); + e.status.expect("help returned an error"); + } + + const MAIN_HELP_STRING: &str = r###"Usage: cmdname <command> [<args>] + +A type for testing `--help`/`help` + +Options: + --help display usage information + +Commands: + first First subcommmand for testing `help`. +"###; + + const FIRST_HELP_STRING: &str = r###"Usage: cmdname first <command> [<args>] + +First subcommmand for testing `help`. + +Options: + --help display usage information + +Commands: + second Second subcommand for testing `help`. +"###; + + const SECOND_HELP_STRING: &str = r###"Usage: cmdname first second + +Second subcommand for testing `help`. + +Options: + --help display usage information +"###; + + #[test] + fn help_keyword_main() { + expect_help(&["help"], MAIN_HELP_STRING) + } + + #[test] + fn help_keyword_with_following_subcommand() { + expect_help(&["help", "first"], FIRST_HELP_STRING); + } + + #[test] + fn help_keyword_between_subcommands() { + expect_help(&["first", "help", "second"], SECOND_HELP_STRING); + } + + #[test] + fn help_keyword_with_two_trailing_subcommands() { + expect_help(&["help", "first", "second"], SECOND_HELP_STRING); + } + + #[test] + fn help_flag_main() { + expect_help(&["--help"], MAIN_HELP_STRING); + } + + #[test] + fn help_flag_subcommand() { + expect_help(&["first", "--help"], FIRST_HELP_STRING); + } + + #[test] + fn help_flag_trailing_arguments_are_an_error() { + let e = OneOption::from_args(&["cmdname"], &["--help", "--foo", "bar"]) + .expect_err("should exit early"); + assert_eq!("Trailing arguments are not allowed after `help`.", e.output); + e.status.expect_err("should be an error"); + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh( + description = "Destroy the contents of <file>.", + example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", + note = "Use `{command_name} help <command>` for details on [<args>] for a subcommand.", + error_code(2, "The blade is too dull."), + error_code(3, "Out of fuel.") + )] + struct HelpExample { + /// force, ignore minor errors. This description is so long that it wraps to the next line. + #[argh(switch, short = 'f')] + force: bool, + + /// documentation + #[argh(switch)] + really_really_really_long_name_for_pat: bool, + + /// write <scribble> repeatedly + #[argh(option, short = 's')] + scribble: String, + + /// say more. Defaults to $BLAST_VERBOSE. + #[argh(switch, short = 'v')] + verbose: bool, + + #[argh(subcommand)] + command: HelpExampleSubCommands, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum HelpExampleSubCommands { + BlowUp(BlowUp), + Grind(GrindCommand), + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "blow-up")] + /// explosively separate + struct BlowUp { + /// blow up bombs safely + #[argh(switch)] + safely: bool, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "grind", description = "make smaller by many small cuts")] + struct GrindCommand { + /// wear a visor while grinding + #[argh(switch)] + safely: bool, + } + + #[test] + fn example_parses_correctly() { + let help_example = HelpExample::from_args( + &["program-name"], + &["-f", "--scribble", "fooey", "blow-up", "--safely"], + ) + .unwrap(); + + assert_eq!( + help_example, + HelpExample { + force: true, + scribble: "fooey".to_string(), + really_really_really_long_name_for_pat: false, + verbose: false, + command: HelpExampleSubCommands::BlowUp(BlowUp { safely: true }), + }, + ); + } + + #[test] + fn example_errors_on_missing_required_option_and_missing_required_subcommand() { + let exit = HelpExample::from_args(&["program-name"], &[]).unwrap_err(); + exit.status.unwrap_err(); + assert_eq!( + exit.output, + concat!( + "Required options not provided:\n", + " --scribble\n", + "One of the following subcommands must be present:\n", + " help\n", + " blow-up\n", + " grind\n", + ), + ); + } + + #[test] + fn help_example() { + assert_help_string::<HelpExample>( + r###"Usage: test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s <scribble> [-v] <command> [<args>] + +Destroy the contents of <file>. + +Options: + -f, --force force, ignore minor errors. This description is so long that + it wraps to the next line. + --really-really-really-long-name-for-pat + documentation + -s, --scribble write <scribble> repeatedly + -v, --verbose say more. Defaults to $BLAST_VERBOSE. + --help display usage information + +Commands: + blow-up explosively separate + grind make smaller by many small cuts + +Examples: + Scribble 'abc' and then run |grind|. + $ test_arg_0 -s 'abc' grind old.txt taxes.cp + +Notes: + Use `test_arg_0 help <command>` for details on [<args>] for a subcommand. + +Error codes: + 2 The blade is too dull. + 3 Out of fuel. +"###, + ); + } + + #[allow(dead_code)] + #[derive(argh::FromArgs)] + /// Destroy the contents of <file>. + struct WithArgName { + #[argh(positional, arg_name = "name")] + username: String, + } + + #[test] + fn with_arg_name() { + assert_help_string::<WithArgName>( + r###"Usage: test_arg_0 <name> + +Destroy the contents of <file>. + +Positional Arguments: + name + +Options: + --help display usage information +"###, + ); + } +} + +#[test] +fn redact_arg_values_no_args() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// a msg param + _msg: Option<String>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &[]).unwrap(); + assert_eq!(actual, &["program-name"]); +} + +#[test] +fn redact_arg_values_optional_arg() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// a msg param + _msg: Option<String>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["--msg", "hello"]).unwrap(); + assert_eq!(actual, &["program-name", "--msg"]); +} + +#[test] +fn redact_arg_values_optional_arg_short() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option, short = 'm')] + /// a msg param + _msg: Option<String>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["-m", "hello"]).unwrap(); + assert_eq!(actual, &["program-name", "-m"]); +} + +#[test] +fn redact_arg_values_optional_arg_long() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option, long = "my-msg")] + /// a msg param + _msg: Option<String>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["--my-msg", "hello"]).unwrap(); + assert_eq!(actual, &["program-name", "--my-msg"]); +} + +#[test] +fn redact_arg_values_two_option_args() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// a msg param + _msg: String, + + #[argh(option)] + /// a delivery param + _delivery: String, + } + + let actual = + Cmd::redact_arg_values(&["program-name"], &["--msg", "hello", "--delivery", "next day"]) + .unwrap(); + assert_eq!(actual, &["program-name", "--msg", "--delivery"]); +} + +#[test] +fn redact_arg_values_option_one_optional_args() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// a msg param + _msg: String, + + #[argh(option)] + /// a delivery param + _delivery: Option<String>, + } + + let actual = + Cmd::redact_arg_values(&["program-name"], &["--msg", "hello", "--delivery", "next day"]) + .unwrap(); + assert_eq!(actual, &["program-name", "--msg", "--delivery"]); + + let actual = Cmd::redact_arg_values(&["program-name"], &["--msg", "hello"]).unwrap(); + assert_eq!(actual, &["program-name", "--msg"]); +} + +#[test] +fn redact_arg_values_option_repeating() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(option)] + /// fooey + _msg: Vec<String>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &[]).unwrap(); + assert_eq!(actual, &["program-name"]); + + let actual = + Cmd::redact_arg_values(&["program-name"], &["--msg", "abc", "--msg", "xyz"]).unwrap(); + assert_eq!(actual, &["program-name", "--msg", "--msg"]); +} + +#[test] +fn redact_arg_values_switch() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(switch, short = 'f')] + /// speed of cmd + _faster: bool, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["--faster"]).unwrap(); + assert_eq!(actual, &["program-name", "--faster"]); + + let actual = Cmd::redact_arg_values(&["program-name"], &["-f"]).unwrap(); + assert_eq!(actual, &["program-name", "-f"]); +} + +#[test] +fn redact_arg_values_positional() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[allow(unused)] + #[argh(positional)] + /// speed of cmd + speed: u8, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap(); + assert_eq!(actual, &["program-name", "speed"]); +} + +#[test] +fn redact_arg_values_positional_arg_name() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap(); + assert_eq!(actual, &["program-name", "speed"]); +} + +#[test] +fn redact_arg_values_positional_repeating() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: Vec<u8>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["5", "6"]).unwrap(); + assert_eq!(actual, &["program-name", "speed", "speed"]); +} + +#[test] +fn redact_arg_values_positional_err() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &[]).unwrap_err(); + assert_eq!( + actual, + argh::EarlyExit { + output: "Required positional arguments not provided:\n speed\n".into(), + status: Err(()), + } + ); +} + +#[test] +fn redact_arg_values_two_positional() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + + #[argh(positional, arg_name = "direction")] + /// direction + _direction: String, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["5", "north"]).unwrap(); + assert_eq!(actual, &["program-name", "speed", "direction"]); +} + +#[test] +fn redact_arg_values_positional_option() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + + #[argh(option)] + /// direction + _direction: String, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["5", "--direction", "north"]).unwrap(); + assert_eq!(actual, &["program-name", "speed", "--direction"]); +} + +#[test] +fn redact_arg_values_positional_optional_option() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + + #[argh(option)] + /// direction + _direction: Option<String>, + } + + let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap(); + assert_eq!(actual, &["program-name", "speed"]); +} + +#[test] +fn redact_arg_values_subcommand() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + + #[argh(subcommand)] + /// means of transportation + _means: MeansSubcommand, + } + + #[derive(FromArgs, Debug)] + /// Short description + #[argh(subcommand)] + enum MeansSubcommand { + Walking(WalkingSubcommand), + Biking(BikingSubcommand), + Driving(DrivingSubcommand), + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "walking")] + /// Short description + struct WalkingSubcommand { + #[argh(option)] + /// a song to listen to + _music: String, + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "biking")] + /// Short description + struct BikingSubcommand {} + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "driving")] + /// short description + struct DrivingSubcommand {} + + let actual = + Cmd::redact_arg_values(&["program-name"], &["5", "walking", "--music", "Bach"]).unwrap(); + assert_eq!(actual, &["program-name", "speed", "walking", "--music"]); +} + +#[test] +fn redact_arg_values_subcommand_with_space_in_name() { + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional, arg_name = "speed")] + /// speed of cmd + _speed: u8, + + #[argh(subcommand)] + /// means of transportation + _means: MeansSubcommand, + } + + #[derive(FromArgs, Debug)] + /// Short description + #[argh(subcommand)] + enum MeansSubcommand { + Walking(WalkingSubcommand), + Biking(BikingSubcommand), + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "has space")] + /// Short description + struct WalkingSubcommand { + #[argh(option)] + /// a song to listen to + _music: String, + } + + #[derive(FromArgs, Debug)] + #[argh(subcommand, name = "biking")] + /// Short description + struct BikingSubcommand {} + + let actual = + Cmd::redact_arg_values(&["program-name"], &["5", "has space", "--music", "Bach"]).unwrap(); + assert_eq!(actual, &["program-name", "speed", "has space", "--music"]); +} + +#[test] +fn redact_arg_values_produces_help() { + #[derive(argh::FromArgs, Debug, PartialEq)] + /// Woot + struct Repeating { + #[argh(option, short = 'n')] + /// fooey + n: Vec<String>, + } + + assert_eq!( + Repeating::redact_arg_values(&["program-name"], &["--help"]), + Err(argh::EarlyExit { + output: r###"Usage: program-name [-n <n...>] + +Woot + +Options: + -n, --n fooey + --help display usage information +"### + .to_string(), + status: Ok(()), + }), + ); +} + +#[test] +fn redact_arg_values_produces_errors_with_bad_arguments() { + #[derive(argh::FromArgs, Debug, PartialEq)] + /// Woot + struct Cmd { + #[argh(option, short = 'n')] + /// fooey + n: String, + } + + assert_eq!( + Cmd::redact_arg_values(&["program-name"], &["--n"]), + Err(argh::EarlyExit { + output: "No value provided for option '--n'.\n".to_string(), + status: Err(()), + }), + ); +} + +#[test] +fn redact_arg_values_does_not_warn_if_used() { + #[forbid(unused)] + #[derive(FromArgs, Debug)] + /// Short description + struct Cmd { + #[argh(positional)] + /// speed of cmd + speed: u8, + } + + let cmd = Cmd::from_args(&["program-name"], &["5"]).unwrap(); + assert_eq!(cmd.speed, 5); + + let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap(); + assert_eq!(actual, &["program-name", "speed"]); +} |