aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Vander Stoep <jeffv@google.com>2022-12-06 11:22:20 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-12-06 11:22:20 +0000
commit149af1ab88f136611ab0e6303b480c7ea814619a (patch)
treeb3542f15f44f8d53a3ec7b0a71275f6569772f64
parent0f334898a7405c9f50bffae15457b520fe63fac3 (diff)
parent3c937e614018e1d33f04b0574eccfeaf51f13122 (diff)
downloadargh-149af1ab88f136611ab0e6303b480c7ea814619a.tar.gz
Upgrade argh to 0.1.9 am: 3c937e6140main-16k-with-phones
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/argh/+/2327636 Change-Id: Id18aa0a115297372215ce0646ac1c1647dfe0a1b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.cargo_vcs_info.json2
-rw-r--r--Android.bp12
-rw-r--r--Cargo.lock.saved189
-rw-r--r--Cargo.toml26
-rw-r--r--Cargo.toml.orig10
-rw-r--r--METADATA12
-rw-r--r--README.md13
-rw-r--r--cargo2android.json10
-rw-r--r--examples/simple_example.rs42
-rw-r--r--src/lib.rs220
-rw-r--r--tests/compiletest.rs5
-rw-r--r--tests/lib.rs349
-rw-r--r--tests/ui/conflicting-tails/positional-and-greedy.rs13
-rw-r--r--tests/ui/conflicting-tails/positional-and-greedy.stderr11
-rw-r--r--tests/ui/duplicate-name/duplicate-long-name.rs21
-rw-r--r--tests/ui/duplicate-name/duplicate-long-name.stderr31
-rw-r--r--tests/ui/duplicate-name/duplicate-short-name.rs21
-rw-r--r--tests/ui/duplicate-name/duplicate-short-name.stderr31
18 files changed, 964 insertions, 54 deletions
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 <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"]
+version = "0.1.9"
+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"]
+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 <cramertj@google.com>", "Benjamin Brittain <bwb@google.com>", "Erick Tryzelaar <etryzelaar@google.com>"]
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<String>,
//! }
//!
-//! 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<String>,
+//! #[argh(positional, greedy)]
+//! all_the_rest: Vec<String>,
+//! }
+//! ```
+//!
+//! 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<Vec<&'static CommandInfo>> = 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<Result<Vec<String>, 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<Result<Self, EarlyExit>> {
+//! 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<T: SubCommand> 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<Result<Vec<String>, 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<Result<Self, EarlyExit>>;
+}
+
/// 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<String> 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: TopLevelCommand>() -> 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: TopLevelCommand>() -> 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<bool, 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 {
+ 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<bool, EarlyExit> {
- 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<Item = &'a CommandInfo>) -> 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<Vec<&'static CommandInfo>>,
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<Item = &'static CommandInfo>) {
+ 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]
@@ -28,6 +36,34 @@ fn basic_example() {
}
#[test]
+fn generic_example() {
+ use std::fmt::Display;
+ use std::str::FromStr;
+
+ #[derive(FromArgs, PartialEq, Debug)]
+ /// Reach new heights.
+ struct GoUp<S: FromStr>
+ where
+ <S as FromStr>::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<S>,
+ }
+
+ let up = GoUp::<String>::from_args(&["cmdname"], &["--height", "5"]).expect("failed go_up");
+ assert_eq!(up, GoUp::<String> { jump: false, height: 5, pilot_nickname: None });
+}
+
+#[test]
fn custom_from_str_example() {
#[derive(FromArgs)]
/// Goofy thing.
@@ -87,6 +123,119 @@ fn subcommand_example() {
}
#[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<Result<Vec<String>, argh::EarlyExit>> {
+ Some(Err(argh::EarlyExit::from("Test should not redact".to_owned())))
+ }
+
+ fn try_from_args(
+ command_name: &[&str],
+ args: &[&str],
+ ) -> Option<Result<DynamicSubCommandImpl, argh::EarlyExit>> {
+ 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)]
/// Short description
@@ -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)]
@@ -311,6 +460,90 @@ Options:
#[derive(FromArgs, Debug, PartialEq)]
/// Woot
+ struct LastRepeatingGreedy {
+ #[argh(positional)]
+ /// fooey
+ a: u32,
+ #[argh(switch)]
+ /// woo
+ b: bool,
+ #[argh(option)]
+ /// stuff
+ c: Option<String>,
+ #[argh(positional, greedy)]
+ /// fooey
+ d: Vec<String>,
+ }
+
+ #[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::<LastRepeatingGreedy>(
+ r###"Usage: test_arg_0 <a> [--b] [--c <c>] [d...]
+
+Woot
+
+Positional Arguments:
+ a fooey
+
+Options:
+ --b woo
+ --c stuff
+ --help display usage information
+"###,
+ );
+ }
+
+ #[derive(FromArgs, Debug, PartialEq)]
+ /// Woot
struct LastOptional {
#[argh(positional)]
/// fooey
@@ -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<Result<Vec<String>, argh::EarlyExit>> {
+ Some(Err(argh::EarlyExit::from("Test should not redact".to_owned())))
+ }
+
+ fn try_from_args(
+ command_name: &[&str],
+ args: &[&str],
+ ) -> Option<Result<HelpExamplePlugin, argh::EarlyExit>> {
+ 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<String>,
+
+ #[argh(positional, greedy)]
+ /// remainder
+ remainder: Vec<String>,
+}
+
+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,
+ | |_____________^