From 3c937e614018e1d33f04b0574eccfeaf51f13122 Mon Sep 17 00:00:00 2001 From: Jeff Vander Stoep Date: Mon, 5 Dec 2022 14:54:44 +0100 Subject: Upgrade argh to 0.1.9 This project was upgraded with external_updater. Usage: tools/external_updater/updater.sh update rust/crates/argh For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md Test: TreeHugger Change-Id: I3b9c2a1bc2475c81b50e5945a35b8135fa82d584 --- .cargo_vcs_info.json | 2 +- Android.bp | 12 +- Cargo.lock.saved | 189 +++++++++++ Cargo.toml | 26 +- Cargo.toml.orig | 10 +- METADATA | 12 +- README.md | 13 + cargo2android.json | 10 +- examples/simple_example.rs | 42 +++ src/lib.rs | 220 +++++++++++-- tests/compiletest.rs | 5 + tests/lib.rs | 349 ++++++++++++++++++++- .../ui/conflicting-tails/positional-and-greedy.rs | 13 + .../conflicting-tails/positional-and-greedy.stderr | 11 + tests/ui/duplicate-name/duplicate-long-name.rs | 21 ++ tests/ui/duplicate-name/duplicate-long-name.stderr | 31 ++ tests/ui/duplicate-name/duplicate-short-name.rs | 21 ++ .../ui/duplicate-name/duplicate-short-name.stderr | 31 ++ 18 files changed, 964 insertions(+), 54 deletions(-) create mode 100644 Cargo.lock.saved create mode 100644 examples/simple_example.rs create mode 100644 tests/compiletest.rs create mode 100644 tests/ui/conflicting-tails/positional-and-greedy.rs create mode 100644 tests/ui/conflicting-tails/positional-and-greedy.stderr create mode 100644 tests/ui/duplicate-name/duplicate-long-name.rs create mode 100644 tests/ui/duplicate-name/duplicate-long-name.stderr create mode 100644 tests/ui/duplicate-name/duplicate-short-name.rs create mode 100644 tests/ui/duplicate-name/duplicate-short-name.stderr diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index 23c489b..13adf5d 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "f1f85d2d89cbe09314dc1b59e581b8a43531cf3e" + "sha1": "adc704cd29f710864b0fc1872bc86f857bebfdbf" }, "path_in_vcs": "argh" } \ No newline at end of file diff --git a/Android.bp b/Android.bp index df3edb1..04c0314 100644 --- a/Android.bp +++ b/Android.bp @@ -1,8 +1,6 @@ // This file is generated by cargo2android.py --config cargo2android.json. // Do not modify this file as changes will be overridden on upgrade. - - package { default_applicable_licenses: ["external_rust_crates_argh_license"], } @@ -25,7 +23,7 @@ rust_test { host_supported: true, crate_name: "argh", cargo_env_compat: true, - cargo_pkg_version: "0.1.7", + cargo_pkg_version: "0.1.9", srcs: ["src/lib.rs"], test_suites: ["general-tests"], auto_gen_config: true, @@ -35,6 +33,7 @@ rust_test { edition: "2018", rustlibs: [ "libargh_shared", + "libonce_cell", ], proc_macros: ["libargh_derive"], } @@ -42,9 +41,9 @@ rust_test { rust_test { name: "argh_test_tests_lib", host_supported: true, - crate_name: "lib", + crate_name: "argh", cargo_env_compat: true, - cargo_pkg_version: "0.1.7", + cargo_pkg_version: "0.1.9", srcs: ["tests/lib.rs"], test_suites: ["general-tests"], auto_gen_config: true, @@ -55,6 +54,7 @@ rust_test { rustlibs: [ "libargh", "libargh_shared", + "libonce_cell", ], proc_macros: ["libargh_derive"], } @@ -64,7 +64,7 @@ rust_library { host_supported: true, crate_name: "argh", cargo_env_compat: true, - cargo_pkg_version: "0.1.7", + cargo_pkg_version: "0.1.9", srcs: ["src/lib.rs"], edition: "2018", rustlibs: [ diff --git a/Cargo.lock.saved b/Cargo.lock.saved new file mode 100644 index 0000000..fe2463c --- /dev/null +++ b/Cargo.lock.saved @@ -0,0 +1,189 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "argh" +version = "0.1.9" +dependencies = [ + "argh_derive", + "argh_shared", + "once_cell", + "trybuild", +] + +[[package]] +name = "argh_derive" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa013479b80109a1bf01a039412b0f0013d716f36921226d86c6709032fb7a03" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149f75bbec1827618262e0855a68f0f9a7f2edc13faebf33c4f16d6725edb6a9" + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" + +[[package]] +name = "serde_derive" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "trybuild" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f408301c7480f9e6294eb779cfc907f54bd901a9660ef24d7f233ed5376485" +dependencies = [ + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index b20c0fc..220a383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,15 +12,31 @@ [package] edition = "2018" name = "argh" -version = "0.1.7" -authors = ["Taylor Cramer ", "Benjamin Brittain ", "Erick Tryzelaar "] +version = "0.1.9" +authors = [ + "Taylor Cramer ", + "Benjamin Brittain ", + "Erick Tryzelaar ", +] description = "Derive-based argument parser optimized for code size" readme = "README.md" -keywords = ["args", "arguments", "derive", "cli"] +keywords = [ + "args", + "arguments", + "derive", + "cli", +] license = "BSD-3-Clause" repository = "https://github.com/google/argh" + [dependencies.argh_derive] -version = "0.1.7" +version = "0.1.9" [dependencies.argh_shared] -version = "0.1.7" +version = "0.1.9" + +[dev-dependencies.once_cell] +version = "1.10.0" + +[dev-dependencies.trybuild] +version = "1.0.63" diff --git a/Cargo.toml.orig b/Cargo.toml.orig index ec92149..4b3bb43 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "argh" -version = "0.1.7" +version = "0.1.9" authors = ["Taylor Cramer ", "Benjamin Brittain ", "Erick Tryzelaar "] edition = "2018" keywords = ["args", "arguments", "derive", "cli"] @@ -10,5 +10,9 @@ 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" } +argh_shared = { version = "0.1.9", path = "../argh_shared" } +argh_derive = { version = "0.1.9", path = "../argh_derive" } + +[dev-dependencies] +once_cell = "1.10.0" +trybuild = "1.0.63" diff --git a/METADATA b/METADATA index a9d08d0..4a58820 100644 --- a/METADATA +++ b/METADATA @@ -1,3 +1,7 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update rust/crates/argh +# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md + name: "argh" description: "Derive-based argument parser optimized for code size" third_party { @@ -7,13 +11,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/argh/argh-0.1.7.crate" + value: "https://static.crates.io/crates/argh/argh-0.1.9.crate" } - version: "0.1.7" + version: "0.1.9" license_type: NOTICE last_upgrade_date { year: 2022 - month: 1 - day: 13 + month: 12 + day: 5 } } diff --git a/README.md b/README.md index 4e949e4..7368162 100644 --- a/README.md +++ b/README.md @@ -175,3 +175,16 @@ struct SubCommandTwo { ``` NOTE: This is not an officially supported Google product. + + +## How to debug the expanded derive macro for `argh` + +The `argh::FromArgs` derive macro can be debugged with the [cargo-expand](https://crates.io/crates/cargo-expand) crate. + +### Expand the derive macro in `examples/simple_example.rs` + +See [argh/examples/simple_example.rs](./argh/examples/simple_example.rs) for the example struct we wish to expand. + +First, install `cargo-expand` by running `cargo install cargo-expand`. Note this requires the nightly build of Rust. + +Once installed, run `cargo expand` with in the `argh` package and you can see the expanded code. diff --git a/cargo2android.json b/cargo2android.json index 6e516e0..1dd19e7 100644 --- a/cargo2android.json +++ b/cargo2android.json @@ -5,5 +5,11 @@ ], "device": true, "run": true, - "tests": true -} \ No newline at end of file + "tests": true, + "dependency-blocklist": [ + "trybuild" + ], + "test-blocklist": [ + "tests/compiletest.rs" + ] +} diff --git a/examples/simple_example.rs b/examples/simple_example.rs new file mode 100644 index 0000000..d977495 --- /dev/null +++ b/examples/simple_example.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2022 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}; + +#[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, +} + +fn main() { + let toplevel: TopLevel = argh::from_env(); + println!("{:#?}", toplevel); +} diff --git a/src/lib.rs b/src/lib.rs index 984d927..c507be0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,9 +31,7 @@ //! pilot_nickname: Option, //! } //! -//! fn main() { -//! let up: GoUp = argh::from_env(); -//! } +//! let up: GoUp = argh::from_env(); //! ``` //! //! `./some_bin --help` will then output the following: @@ -129,6 +127,34 @@ //! The last positional argument may include a default, or be wrapped in //! `Option` or `Vec` to indicate an optional or repeating positional argument. //! +//! If your final positional argument has the `greedy` option on it, it will consume +//! any arguments after it as if a `--` were placed before the first argument to +//! match the greedy positional: +//! +//! ```rust +//! use argh::FromArgs; +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// A command with a greedy positional argument at the end. +//! struct WithGreedyPositional { +//! /// some stuff +//! #[argh(option)] +//! stuff: Option, +//! #[argh(positional, greedy)] +//! all_the_rest: Vec, +//! } +//! ``` +//! +//! Now if you pass `--stuff Something` after a positional argument, it will +//! be consumed by `all_the_rest` instead of setting the `stuff` field. +//! +//! Note that `all_the_rest` won't be listed as a positional argument in the +//! long text part of help output (and it will be listed at the end of the usage +//! line as `[all_the_rest...]`), and it's up to the caller to append any +//! extra help output for the meaning of the captured arguments. This is to +//! enable situations where some amount of argument processing needs to happen +//! before the rest of the arguments can be interpreted, and shouldn't be used +//! for regular use as it might be confusing. +//! //! 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: @@ -168,6 +194,102 @@ //! fooey: bool, //! } //! ``` +//! +//! 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 +//! dynamic variant should implement `DynamicSubCommand`. +//! +//! ```rust +//! # use argh::CommandInfo; +//! # use argh::DynamicSubCommand; +//! # use argh::EarlyExit; +//! # use argh::FromArgs; +//! # use once_cell::sync::OnceCell; +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// Top-level command. +//! struct TopLevel { +//! #[argh(subcommand)] +//! nested: MySubCommandEnum, +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! #[argh(subcommand)] +//! enum MySubCommandEnum { +//! Normal(NormalSubCommand), +//! #[argh(dynamic)] +//! Dynamic(Dynamic), +//! } +//! +//! #[derive(FromArgs, PartialEq, Debug)] +//! /// Normal subcommand. +//! #[argh(subcommand, name = "normal")] +//! struct NormalSubCommand { +//! #[argh(option)] +//! /// how many x +//! x: usize, +//! } +//! +//! /// Dynamic subcommand. +//! #[derive(PartialEq, Debug)] +//! struct Dynamic { +//! name: String +//! } +//! +//! impl DynamicSubCommand for Dynamic { +//! fn commands() -> &'static [&'static CommandInfo] { +//! static RET: OnceCell> = OnceCell::new(); +//! RET.get_or_init(|| { +//! let mut commands = Vec::new(); +//! +//! // argh needs the `CommandInfo` structs we generate to be valid +//! // for the static lifetime. We can allocate the structures on +//! // the heap with `Box::new` and use `Box::leak` to get a static +//! // reference to them. We could also just use a constant +//! // reference, but only because this is a synthetic example; the +//! // point of using dynamic commands is to have commands you +//! // don't know about until runtime! +//! commands.push(&*Box::leak(Box::new(CommandInfo { +//! name: "dynamic_command", +//! description: "A dynamic command", +//! }))); +//! +//! commands +//! }) +//! } +//! +//! fn try_redact_arg_values( +//! command_name: &[&str], +//! args: &[&str], +//! ) -> Option, EarlyExit>> { +//! for command in Self::commands() { +//! if command_name.last() == Some(&command.name) { +//! // Process arguments and redact values here. +//! if !args.is_empty() { +//! return Some(Err("Our example dynamic command never takes arguments!" +//! .to_string().into())); +//! } +//! return Some(Ok(Vec::new())) +//! } +//! } +//! None +//! } +//! +//! fn try_from_args(command_name: &[&str], args: &[&str]) -> Option> { +//! for command in Self::commands() { +//! if command_name.last() == Some(&command.name) { +//! if !args.is_empty() { +//! return Some(Err("Our example dynamic command never takes arguments!" +//! .to_string().into())); +//! } +//! return Some(Ok(Dynamic { name: command.name.to_string() })) +//! } +//! } +//! None +//! } +//! } +//! ``` #![deny(missing_docs)] @@ -445,6 +567,11 @@ pub trait TopLevelCommand: FromArgs {} pub trait SubCommands: FromArgs { /// Info for the commands. const COMMANDS: &'static [&'static CommandInfo]; + + /// Get a list of commands that are discovered at runtime. + fn dynamic_commands() -> &'static [&'static CommandInfo] { + &[] + } } /// A `FromArgs` implementation that represents a single subcommand. @@ -457,6 +584,34 @@ impl SubCommands for T { const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND]; } +/// Trait implemented by values returned from a dynamic subcommand handler. +pub trait DynamicSubCommand: Sized { + /// Info about supported subcommands. + fn commands() -> &'static [&'static CommandInfo]; + + /// Perform the function of `FromArgs::redact_arg_values` for this dynamic + /// command. + /// + /// The full list of subcommands, ending with the subcommand that should be + /// dynamically recognized, is passed in `command_name`. If the command + /// passed is not recognized, this function should return `None`. Otherwise + /// it should return `Some`, and the value within the `Some` has the same + /// semantics as the return of `FromArgs::redact_arg_values`. + fn try_redact_arg_values( + command_name: &[&str], + args: &[&str], + ) -> Option, EarlyExit>>; + + /// Perform the function of `FromArgs::from_args` for this dynamic command. + /// + /// The full list of subcommands, ending with the subcommand that should be + /// dynamically recognized, is passed in `command_name`. If the command + /// passed is not recognized, this function should return `None`. Otherwise + /// it should return `Some`, and the value within the `Some` has the same + /// semantics as the return of `FromArgs::from_args`. + fn try_from_args(command_name: &[&str], args: &[&str]) -> Option>; +} + /// 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`. @@ -481,8 +636,8 @@ impl From for EarlyExit { } /// 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()) +fn cmd<'a>(default: &'a str, path: &'a str) -> &'a str { + std::path::Path::new(path).file_name().and_then(|s| s.to_str()).unwrap_or(default) } /// Create a `FromArgs` type from the current process's `env::args`. @@ -501,7 +656,7 @@ pub fn from_env() -> T { 0 } Err(()) => { - eprintln!("{}", early_exit.output); + eprintln!("{}\nRun {} --help for more information.", early_exit.output, cmd); 1 } }) @@ -527,7 +682,7 @@ pub fn cargo_from_env() -> T { 0 } Err(()) => { - eprintln!("{}", early_exit.output); + eprintln!("{}\nRun --help for more information.", early_exit.output); 1 } }) @@ -683,14 +838,14 @@ pub fn parse_struct_args( let mut positional_index = 0; let mut options_ended = false; - 'parse_args: while let Some(&next_arg) = remaining_args.get(0) { + 'parse_args: while let Some(&next_arg) = remaining_args.first() { 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.starts_with('-') && !options_ended { if next_arg == "--" { options_ended = true; continue; @@ -712,7 +867,7 @@ pub fn parse_struct_args( } } - parse_positionals.parse(&mut positional_index, next_arg)?; + options_ended |= parse_positionals.parse(&mut positional_index, next_arg)?; } if help { @@ -751,7 +906,7 @@ impl<'a> ParseStructOptions<'a> { ParseStructOption::Flag(ref mut b) => b.set_flag(arg), ParseStructOption::Value(ref mut pvs) => { let value = remaining_args - .get(0) + .first() .ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?; *remaining_args = &remaining_args[1..]; pvs.fill_slot(arg, value).map_err(|s| { @@ -783,25 +938,31 @@ pub enum ParseStructOption<'a> { pub struct ParseStructPositionals<'a> { pub positionals: &'a mut [ParseStructPositional<'a>], pub last_is_repeating: bool, + pub last_is_greedy: 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> { + /// + /// Returns true if non-positional argument parsing should stop + /// after this one. + fn parse(&mut self, index: &mut usize, arg: &str) -> Result { 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 { + if self.last_is_repeating && *index == self.positionals.len() - 1 { + // Don't increment position if we're at the last arg + // *and* the last arg is repeating. If it's also remainder, + // halt non-option processing after this. + Ok(self.last_is_greedy) + } else { + // If it is repeating, though, increment the index and continue + // processing options. *index += 1; + Ok(false) } - - Ok(()) } else { Err(EarlyExit { output: unrecognized_arg(arg), status: Err(()) }) } @@ -847,7 +1008,10 @@ pub struct ParseStructSubCommand<'a> { // The subcommand commands pub subcommands: &'static [&'static CommandInfo], + pub dynamic_subcommands: &'a [&'static CommandInfo], + // The function to parse the subcommand arguments. + #[allow(clippy::type_complexity)] pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>, } @@ -859,7 +1023,7 @@ impl<'a> ParseStructSubCommand<'a> { arg: &str, remaining_args: &[&str], ) -> Result { - for subcommand in self.subcommands { + for subcommand in self.subcommands.iter().chain(self.dynamic_subcommands.iter()) { if subcommand.name == arg { let mut command = cmd_name.to_owned(); command.push(subcommand.name); @@ -877,7 +1041,7 @@ impl<'a> ParseStructSubCommand<'a> { } } - return Ok(false); + Ok(false) } } @@ -888,7 +1052,7 @@ fn prepend_help<'a>(args: &[&'a str]) -> Vec<&'a str> { } #[doc(hidden)] -pub fn print_subcommands(commands: &[&CommandInfo]) -> String { +pub fn print_subcommands<'a>(commands: impl Iterator) -> String { let mut out = String::new(); for cmd in commands { argh_shared::write_description(&mut out, cmd); @@ -905,7 +1069,7 @@ fn unrecognized_arg(arg: &str) -> String { #[derive(Default)] pub struct MissingRequirements { options: Vec<&'static str>, - subcommands: Option<&'static [&'static CommandInfo]>, + subcommands: Option>, positional_args: Vec<&'static str>, } @@ -920,8 +1084,8 @@ impl MissingRequirements { // Add a missing required subcommand. #[doc(hidden)] - pub fn missing_subcommands(&mut self, commands: &'static [&'static CommandInfo]) { - self.subcommands = Some(commands); + pub fn missing_subcommands(&mut self, commands: impl Iterator) { + self.subcommands = Some(commands.collect()); } // Add a missing positional argument. @@ -951,7 +1115,7 @@ impl MissingRequirements { if !self.options.is_empty() { if !self.positional_args.is_empty() { - output.push_str("\n"); + output.push('\n'); } output.push_str("Required options not provided:"); for option in &self.options { @@ -960,9 +1124,9 @@ impl MissingRequirements { } } - if let Some(missing_subcommands) = self.subcommands { + if let Some(missing_subcommands) = &self.subcommands { if !self.options.is_empty() { - output.push_str("\n"); + output.push('\n'); } output.push_str("One of the following subcommands must be present:"); output.push_str(NEWLINE_INDENT); diff --git a/tests/compiletest.rs b/tests/compiletest.rs new file mode 100644 index 0000000..bd72ecd --- /dev/null +++ b/tests/compiletest.rs @@ -0,0 +1,5 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/**/*.rs"); +} diff --git a/tests/lib.rs b/tests/lib.rs index fe8c858..584897c 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,8 +1,16 @@ -#![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. +// Deny a bunch of uncommon clippy lints to make sure the generated code won't trigger a warning. +#![deny( + clippy::indexing_slicing, + clippy::panic_in_result_fn, + clippy::str_to_string, + clippy::unreachable, + clippy::unwrap_in_result +)] + use {argh::FromArgs, std::fmt::Debug}; #[test] @@ -27,6 +35,34 @@ fn basic_example() { assert_eq!(up, GoUp { jump: false, height: 5, pilot_nickname: None }); } +#[test] +fn generic_example() { + use std::fmt::Display; + use std::str::FromStr; + + #[derive(FromArgs, PartialEq, Debug)] + /// Reach new heights. + struct GoUp + where + ::Err: Display, + { + /// 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, + } + + 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)] @@ -86,6 +122,119 @@ fn subcommand_example() { assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); } +#[test] +fn dynamic_subcommand_example() { + #[derive(PartialEq, Debug)] + struct DynamicSubCommandImpl { + got: String, + } + + impl argh::DynamicSubCommand for DynamicSubCommandImpl { + fn commands() -> &'static [&'static argh::CommandInfo] { + &[ + &argh::CommandInfo { name: "three", description: "Third command" }, + &argh::CommandInfo { name: "four", description: "Fourth command" }, + &argh::CommandInfo { name: "five", description: "Fifth command" }, + ] + } + + fn try_redact_arg_values( + _command_name: &[&str], + _args: &[&str], + ) -> Option, argh::EarlyExit>> { + Some(Err(argh::EarlyExit::from("Test should not redact".to_owned()))) + } + + fn try_from_args( + command_name: &[&str], + args: &[&str], + ) -> Option> { + let command_name = match command_name.last() { + Some(x) => *x, + None => return Some(Err(argh::EarlyExit::from("No command".to_owned()))), + }; + let description = Self::commands().iter().find(|x| x.name == command_name)?.description; + if args.len() > 1 { + Some(Err(argh::EarlyExit::from("Too many arguments".to_owned()))) + } else if let Some(arg) = args.first() { + Some(Ok(DynamicSubCommandImpl { got: format!("{} got {:?}", description, arg) })) + } else { + Some(Err(argh::EarlyExit::from("Not enough arguments".to_owned()))) + } + } + } + + #[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), + #[argh(dynamic)] + ThreeFourFive(DynamicSubCommandImpl), + } + + #[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 }) },); + + let three = TopLevel::from_args(&["cmdname"], &["three", "beans"]).expect("sc 3"); + assert_eq!( + three, + TopLevel { + nested: MySubCommandEnum::ThreeFourFive(DynamicSubCommandImpl { + got: "Third command got \"beans\"".to_owned() + }) + }, + ); + + let four = TopLevel::from_args(&["cmdname"], &["four", "boulders"]).expect("sc 4"); + assert_eq!( + four, + TopLevel { + nested: MySubCommandEnum::ThreeFourFive(DynamicSubCommandImpl { + got: "Fourth command got \"boulders\"".to_owned() + }) + }, + ); + + let five = TopLevel::from_args(&["cmdname"], &["five", "gold rings"]).expect("sc 5"); + assert_eq!( + five, + TopLevel { + nested: MySubCommandEnum::ThreeFourFive(DynamicSubCommandImpl { + got: "Fifth command got \"gold rings\"".to_owned() + }) + }, + ); +} + #[test] fn multiline_doc_comment_description() { #[derive(FromArgs)] @@ -154,7 +303,7 @@ fn default_number() { fn default_function() { const MSG: &str = "hey I just met you"; fn call_me_maybe() -> String { - MSG.to_string() + MSG.to_owned() } #[derive(FromArgs)] @@ -309,6 +458,90 @@ Options: ); } + #[derive(FromArgs, Debug, PartialEq)] + /// Woot + struct LastRepeatingGreedy { + #[argh(positional)] + /// fooey + a: u32, + #[argh(switch)] + /// woo + b: bool, + #[argh(option)] + /// stuff + c: Option, + #[argh(positional, greedy)] + /// fooey + d: Vec, + } + + #[test] + fn positional_greedy() { + assert_output(&["5"], LastRepeatingGreedy { a: 5, b: false, c: None, d: vec![] }); + assert_output( + &["5", "foo"], + LastRepeatingGreedy { a: 5, b: false, c: None, d: vec!["foo".into()] }, + ); + assert_output( + &["5", "foo", "bar"], + LastRepeatingGreedy { a: 5, b: false, c: None, d: vec!["foo".into(), "bar".into()] }, + ); + assert_output( + &["5", "--b", "foo", "bar"], + LastRepeatingGreedy { a: 5, b: true, c: None, d: vec!["foo".into(), "bar".into()] }, + ); + assert_output( + &["5", "foo", "bar", "--b"], + LastRepeatingGreedy { + a: 5, + b: false, + c: None, + d: vec!["foo".into(), "bar".into(), "--b".into()], + }, + ); + assert_output( + &["5", "--c", "hi", "foo", "bar"], + LastRepeatingGreedy { + a: 5, + b: false, + c: Some("hi".into()), + d: vec!["foo".into(), "bar".into()], + }, + ); + assert_output( + &["5", "foo", "bar", "--c", "hi"], + LastRepeatingGreedy { + a: 5, + b: false, + c: None, + d: vec!["foo".into(), "bar".into(), "--c".into(), "hi".into()], + }, + ); + assert_output( + &["5", "foo", "bar", "--", "hi"], + LastRepeatingGreedy { + a: 5, + b: false, + c: None, + d: vec!["foo".into(), "bar".into(), "--".into(), "hi".into()], + }, + ); + assert_help_string::( + r###"Usage: test_arg_0 [--b] [--c ] [d...] + +Woot + +Positional Arguments: + a fooey + +Options: + --b woo + --c stuff + --help display usage information +"###, + ); + } + #[derive(FromArgs, Debug, PartialEq)] /// Woot struct LastOptional { @@ -790,6 +1023,8 @@ Options: enum HelpExampleSubCommands { BlowUp(BlowUp), Grind(GrindCommand), + #[argh(dynamic)] + Plugin(HelpExamplePlugin), } #[derive(FromArgs, PartialEq, Debug)] @@ -809,6 +1044,39 @@ Options: safely: bool, } + #[derive(PartialEq, Debug)] + struct HelpExamplePlugin { + got: String, + } + + impl argh::DynamicSubCommand for HelpExamplePlugin { + fn commands() -> &'static [&'static argh::CommandInfo] { + &[&argh::CommandInfo { name: "plugin", description: "Example dynamic command" }] + } + + fn try_redact_arg_values( + _command_name: &[&str], + _args: &[&str], + ) -> Option, argh::EarlyExit>> { + Some(Err(argh::EarlyExit::from("Test should not redact".to_owned()))) + } + + fn try_from_args( + command_name: &[&str], + args: &[&str], + ) -> Option> { + if command_name.last() != Some(&"plugin") { + None + } else if args.len() > 1 { + Some(Err(argh::EarlyExit::from("Too many arguments".to_owned()))) + } else if let Some(arg) = args.first() { + Some(Ok(HelpExamplePlugin { got: format!("plugin got {:?}", arg) })) + } else { + Some(Ok(HelpExamplePlugin { got: "plugin got no argument".to_owned() })) + } + } + } + #[test] fn example_parses_correctly() { let help_example = HelpExample::from_args( @@ -821,7 +1089,7 @@ Options: help_example, HelpExample { force: true, - scribble: "fooey".to_string(), + scribble: "fooey".to_owned(), really_really_really_long_name_for_pat: false, verbose: false, command: HelpExampleSubCommands::BlowUp(BlowUp { safely: true }), @@ -842,6 +1110,7 @@ Options: " help\n", " blow-up\n", " grind\n", + " plugin\n", ), ); } @@ -865,6 +1134,7 @@ Options: Commands: blow-up explosively separate grind make smaller by many small cuts + plugin Example dynamic command Examples: Scribble 'abc' and then run |grind|. @@ -1264,7 +1534,7 @@ Options: -n, --n fooey --help display usage information "### - .to_string(), + .to_owned(), status: Ok(()), }), ); @@ -1283,7 +1553,7 @@ fn redact_arg_values_produces_errors_with_bad_arguments() { assert_eq!( Cmd::redact_arg_values(&["program-name"], &["--n"]), Err(argh::EarlyExit { - output: "No value provided for option '--n'.\n".to_string(), + output: "No value provided for option '--n'.\n".to_owned(), status: Err(()), }), ); @@ -1306,3 +1576,72 @@ fn redact_arg_values_does_not_warn_if_used() { let actual = Cmd::redact_arg_values(&["program-name"], &["5"]).unwrap(); assert_eq!(actual, &["program-name", "speed"]); } + +#[test] +fn subcommand_does_not_panic() { + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum SubCommandEnum { + Cmd(SubCommand), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommand { + #[argh(positional)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + // Passing no subcommand name to an emum + assert_eq!( + SubCommandEnum::from_args(&[], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, + ); + + assert_eq!( + SubCommandEnum::redact_arg_values(&[], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, + ); + + // Passing unknown subcommand name to an emum + assert_eq!( + SubCommandEnum::from_args(&["fooey"], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, + ); + + assert_eq!( + SubCommandEnum::redact_arg_values(&["fooey"], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, + ); + + // Passing unknown subcommand name to a struct + assert_eq!( + SubCommand::redact_arg_values(&[], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, + ); +} + +#[test] +fn long_alphanumeric() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(option, long = "ac97")] + /// fooey + ac97: String, + } + + let cmd = Cmd::from_args(&["cmdname"], &["--ac97", "bar"]).unwrap(); + assert_eq!(cmd.ac97, "bar"); +} diff --git a/tests/ui/conflicting-tails/positional-and-greedy.rs b/tests/ui/conflicting-tails/positional-and-greedy.rs new file mode 100644 index 0000000..18c039a --- /dev/null +++ b/tests/ui/conflicting-tails/positional-and-greedy.rs @@ -0,0 +1,13 @@ +/// Command +#[derive(argh::FromArgs)] +struct Cmd { + #[argh(positional)] + /// positional + positional: Vec, + + #[argh(positional, greedy)] + /// remainder + remainder: Vec, +} + +fn main() {} diff --git a/tests/ui/conflicting-tails/positional-and-greedy.stderr b/tests/ui/conflicting-tails/positional-and-greedy.stderr new file mode 100644 index 0000000..f6a6399 --- /dev/null +++ b/tests/ui/conflicting-tails/positional-and-greedy.stderr @@ -0,0 +1,11 @@ +error: Only the last positional argument may be `Option`, `Vec`, or defaulted. + --> tests/ui/conflicting-tails/positional-and-greedy.rs:4:5 + | +4 | #[argh(positional)] + | ^ + +error: Later positional argument declared here. + --> tests/ui/conflicting-tails/positional-and-greedy.rs:8:5 + | +8 | #[argh(positional, greedy)] + | ^ diff --git a/tests/ui/duplicate-name/duplicate-long-name.rs b/tests/ui/duplicate-name/duplicate-long-name.rs new file mode 100644 index 0000000..550a7aa --- /dev/null +++ b/tests/ui/duplicate-name/duplicate-long-name.rs @@ -0,0 +1,21 @@ +/// Command +#[derive(argh::FromArgs)] +struct Cmd { + /// foo1 + #[argh(option, long = "foo")] + foo1: u32, + + /// foo2 + #[argh(option, long = "foo")] + foo2: u32, + + /// bar1 + #[argh(option, long = "bar")] + bar1: u32, + + /// bar2 + #[argh(option, long = "bar")] + bar2: u32, +} + +fn main() {} diff --git a/tests/ui/duplicate-name/duplicate-long-name.stderr b/tests/ui/duplicate-name/duplicate-long-name.stderr new file mode 100644 index 0000000..697f36b --- /dev/null +++ b/tests/ui/duplicate-name/duplicate-long-name.stderr @@ -0,0 +1,31 @@ +error: The long name of "--foo" was already used here. + --> tests/ui/duplicate-name/duplicate-long-name.rs:4:5 + | +4 | / /// foo1 +5 | | #[argh(option, long = "foo")] +6 | | foo1: u32, + | |_____________^ + +error: Later usage here. + --> tests/ui/duplicate-name/duplicate-long-name.rs:8:5 + | +8 | / /// foo2 +9 | | #[argh(option, long = "foo")] +10 | | foo2: u32, + | |_____________^ + +error: The long name of "--bar" was already used here. + --> tests/ui/duplicate-name/duplicate-long-name.rs:12:5 + | +12 | / /// bar1 +13 | | #[argh(option, long = "bar")] +14 | | bar1: u32, + | |_____________^ + +error: Later usage here. + --> tests/ui/duplicate-name/duplicate-long-name.rs:16:5 + | +16 | / /// bar2 +17 | | #[argh(option, long = "bar")] +18 | | bar2: u32, + | |_____________^ diff --git a/tests/ui/duplicate-name/duplicate-short-name.rs b/tests/ui/duplicate-name/duplicate-short-name.rs new file mode 100644 index 0000000..a52bddb --- /dev/null +++ b/tests/ui/duplicate-name/duplicate-short-name.rs @@ -0,0 +1,21 @@ +/// Command +#[derive(argh::FromArgs)] +struct Cmd { + /// foo1 + #[argh(option, short = 'f')] + foo1: u32, + + /// foo2 + #[argh(option, short = 'f')] + foo2: u32, + + /// bar1 + #[argh(option, short = 'b')] + bar1: u32, + + /// bar2 + #[argh(option, short = 'b')] + bar2: u32, +} + +fn main() {} diff --git a/tests/ui/duplicate-name/duplicate-short-name.stderr b/tests/ui/duplicate-name/duplicate-short-name.stderr new file mode 100644 index 0000000..9262a7d --- /dev/null +++ b/tests/ui/duplicate-name/duplicate-short-name.stderr @@ -0,0 +1,31 @@ +error: The short name of "-f" was already used here. + --> tests/ui/duplicate-name/duplicate-short-name.rs:4:5 + | +4 | / /// foo1 +5 | | #[argh(option, short = 'f')] +6 | | foo1: u32, + | |_____________^ + +error: Later usage here. + --> tests/ui/duplicate-name/duplicate-short-name.rs:8:5 + | +8 | / /// foo2 +9 | | #[argh(option, short = 'f')] +10 | | foo2: u32, + | |_____________^ + +error: The short name of "-b" was already used here. + --> tests/ui/duplicate-name/duplicate-short-name.rs:12:5 + | +12 | / /// bar1 +13 | | #[argh(option, short = 'b')] +14 | | bar1: u32, + | |_____________^ + +error: Later usage here. + --> tests/ui/duplicate-name/duplicate-short-name.rs:16:5 + | +16 | / /// bar2 +17 | | #[argh(option, short = 'b')] +18 | | bar2: u32, + | |_____________^ -- cgit v1.2.3