clap/tests/derive/subcommands.rs
Ed Page b7d13dfb88 fix(parser): Prefer invalid subcommands over invalid args
Just having `--help` or `--version` can make us get invalid args instead
of invalid subcommands.   It doesn't make sense to do this unless
positionals are used.  Even then it might not make sense but this is at
least a step in the right direction.

Unsure how I feel about this being backported to clap 3.  It most likely
would be fine?

This was noticed while looking into #4218
2022-09-15 10:30:03 -05:00

583 lines
14 KiB
Rust

// Copyright 2018 Guillaume Pinot (@TeXitoi) <texitoi@texitoi.eu>,
// Kevin Knapp (@kbknapp) <kbknapp@gmail.com>, and
// Ana Hobden (@hoverbear) <operator@hoverbear.org>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// This work was derived from Structopt (https://github.com/TeXitoi/structopt)
// commit#ea76fa1b1b273e65e3b0b1046643715b49bec51f which is licensed under the
// MIT/Apache 2.0 license.
use crate::utils;
use clap::{Args, Parser, Subcommand};
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt {
/// Fetch stuff from GitHub
Fetch {
#[arg(long)]
all: bool,
/// Overwrite local branches.
#[arg(short, long)]
force: bool,
repo: String,
},
Add {
#[arg(short, long)]
interactive: bool,
#[arg(short, long)]
verbose: bool,
},
}
#[test]
fn test_fetch() {
assert_eq!(
Opt::Fetch {
all: true,
force: false,
repo: "origin".to_string()
},
Opt::try_parse_from(&["test", "fetch", "--all", "origin"]).unwrap()
);
assert_eq!(
Opt::Fetch {
all: false,
force: true,
repo: "origin".to_string()
},
Opt::try_parse_from(&["test", "fetch", "-f", "origin"]).unwrap()
);
}
#[test]
fn test_add() {
assert_eq!(
Opt::Add {
interactive: false,
verbose: false
},
Opt::try_parse_from(&["test", "add"]).unwrap()
);
assert_eq!(
Opt::Add {
interactive: true,
verbose: true
},
Opt::try_parse_from(&["test", "add", "-i", "-v"]).unwrap()
);
}
#[test]
fn test_no_parse() {
let result = Opt::try_parse_from(&["test", "badcmd", "-i", "-v"]);
assert!(result.is_err());
let result = Opt::try_parse_from(&["test", "add", "--badoption"]);
assert!(result.is_err());
let result = Opt::try_parse_from(&["test"]);
assert!(result.is_err());
}
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt2 {
DoSomething { arg: String },
}
#[test]
/// This test is specifically to make sure that hyphenated subcommands get
/// processed correctly.
fn test_hyphenated_subcommands() {
assert_eq!(
Opt2::DoSomething {
arg: "blah".to_string()
},
Opt2::try_parse_from(&["test", "do-something", "blah"]).unwrap()
);
}
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt3 {
Add,
Init,
Fetch,
}
#[test]
fn test_null_commands() {
assert_eq!(Opt3::Add, Opt3::try_parse_from(&["test", "add"]).unwrap());
assert_eq!(Opt3::Init, Opt3::try_parse_from(&["test", "init"]).unwrap());
assert_eq!(
Opt3::Fetch,
Opt3::try_parse_from(&["test", "fetch"]).unwrap()
);
}
#[derive(Parser, PartialEq, Eq, Debug)]
#[command(about = "Not shown")]
struct Add {
file: String,
}
/// Not shown
#[derive(Parser, PartialEq, Eq, Debug)]
struct Fetch {
remote: String,
}
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt4 {
// Not shown
/// Add a file
Add(Add),
Init,
/// download history from remote
Fetch(Fetch),
}
#[test]
fn test_tuple_commands() {
assert_eq!(
Opt4::Add(Add {
file: "f".to_string()
}),
Opt4::try_parse_from(&["test", "add", "f"]).unwrap()
);
assert_eq!(Opt4::Init, Opt4::try_parse_from(&["test", "init"]).unwrap());
assert_eq!(
Opt4::Fetch(Fetch {
remote: "origin".to_string()
}),
Opt4::try_parse_from(&["test", "fetch", "origin"]).unwrap()
);
let output = utils::get_long_help::<Opt4>();
assert!(output.contains("download history from remote"));
assert!(output.contains("Add a file"));
assert!(!output.contains("Not shown"));
}
#[test]
fn global_passed_down() {
#[derive(Debug, PartialEq, Eq, Parser)]
struct Opt {
#[arg(global = true, long)]
other: bool,
#[command(subcommand)]
sub: Subcommands,
}
#[derive(Debug, PartialEq, Eq, Subcommand)]
enum Subcommands {
Add,
Global(GlobalCmd),
}
#[derive(Debug, PartialEq, Eq, Args)]
struct GlobalCmd {
#[arg(from_global)]
other: bool,
}
assert_eq!(
Opt::try_parse_from(&["test", "global"]).unwrap(),
Opt {
other: false,
sub: Subcommands::Global(GlobalCmd { other: false })
}
);
assert_eq!(
Opt::try_parse_from(&["test", "global", "--other"]).unwrap(),
Opt {
other: true,
sub: Subcommands::Global(GlobalCmd { other: true })
}
);
}
#[test]
fn external_subcommand() {
#[derive(Debug, PartialEq, Eq, Parser)]
struct Opt {
#[command(subcommand)]
sub: Subcommands,
}
#[derive(Debug, PartialEq, Eq, Subcommand)]
enum Subcommands {
Add,
Remove,
#[command(external_subcommand)]
Other(Vec<String>),
}
assert_eq!(
Opt::try_parse_from(&["test", "add"]).unwrap(),
Opt {
sub: Subcommands::Add
}
);
assert_eq!(
Opt::try_parse_from(&["test", "remove"]).unwrap(),
Opt {
sub: Subcommands::Remove
}
);
assert!(Opt::try_parse_from(&["test"]).is_err());
assert_eq!(
Opt::try_parse_from(&["test", "git", "status"]).unwrap(),
Opt {
sub: Subcommands::Other(vec!["git".into(), "status".into()])
}
);
}
#[test]
fn external_subcommand_os_string() {
use std::ffi::OsString;
#[derive(Debug, PartialEq, Eq, Parser)]
struct Opt {
#[command(subcommand)]
sub: Subcommands,
}
#[derive(Debug, PartialEq, Eq, Subcommand)]
enum Subcommands {
#[command(external_subcommand)]
Other(Vec<OsString>),
}
assert_eq!(
Opt::try_parse_from(&["test", "git", "status"]).unwrap(),
Opt {
sub: Subcommands::Other(vec!["git".into(), "status".into()])
}
);
assert!(Opt::try_parse_from(&["test"]).is_err());
}
#[test]
fn external_subcommand_optional() {
#[derive(Debug, PartialEq, Eq, Parser)]
struct Opt {
#[command(subcommand)]
sub: Option<Subcommands>,
}
#[derive(Debug, PartialEq, Eq, Subcommand)]
enum Subcommands {
#[command(external_subcommand)]
Other(Vec<String>),
}
assert_eq!(
Opt::try_parse_from(&["test", "git", "status"]).unwrap(),
Opt {
sub: Some(Subcommands::Other(vec!["git".into(), "status".into()]))
}
);
assert_eq!(Opt::try_parse_from(&["test"]).unwrap(), Opt { sub: None });
}
#[test]
fn enum_in_enum_subsubcommand() {
#[derive(Parser, Debug, PartialEq, Eq)]
pub enum Opt {
#[command(alias = "l")]
List,
#[command(subcommand, alias = "d")]
Daemon(DaemonCommand),
}
#[derive(Subcommand, Debug, PartialEq, Eq)]
pub enum DaemonCommand {
Start,
Stop,
}
let result = Opt::try_parse_from(&["test"]);
assert!(result.is_err());
let result = Opt::try_parse_from(&["test", "list"]).unwrap();
assert_eq!(Opt::List, result);
let result = Opt::try_parse_from(&["test", "l"]).unwrap();
assert_eq!(Opt::List, result);
let result = Opt::try_parse_from(&["test", "daemon"]);
assert!(result.is_err());
let result = Opt::try_parse_from(&["test", "daemon", "start"]).unwrap();
assert_eq!(Opt::Daemon(DaemonCommand::Start), result);
let result = Opt::try_parse_from(&["test", "d", "start"]).unwrap();
assert_eq!(Opt::Daemon(DaemonCommand::Start), result);
}
#[test]
fn update_subcommands() {
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt {
Command1(Command1),
Command2(Command2),
}
#[derive(Parser, PartialEq, Eq, Debug)]
struct Command1 {
arg1: i32,
arg2: i32,
}
#[derive(Parser, PartialEq, Eq, Debug)]
struct Command2 {
arg2: i32,
}
// Full subcommand update
let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 });
opt.try_update_from(&["test", "command1", "42", "44"])
.unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "command1", "42", "44"]).unwrap(),
opt
);
// Partial subcommand update
let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 });
opt.try_update_from(&["test", "command1", "42"]).unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "command1", "42", "14"]).unwrap(),
opt
);
// Change subcommand
let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 });
opt.try_update_from(&["test", "command2", "43"]).unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "command2", "43"]).unwrap(),
opt
);
}
#[test]
fn update_sub_subcommands() {
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt {
#[command(subcommand)]
Child1(Child1),
#[command(subcommand)]
Child2(Child2),
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
enum Child1 {
Command1(Command1),
Command2(Command2),
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
enum Child2 {
Command1(Command1),
Command2(Command2),
}
#[derive(Args, PartialEq, Eq, Debug)]
struct Command1 {
arg1: i32,
arg2: i32,
}
#[derive(Args, PartialEq, Eq, Debug)]
struct Command2 {
arg2: i32,
}
// Full subcommand update
let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 }));
opt.try_update_from(&["test", "child1", "command1", "42", "44"])
.unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "child1", "command1", "42", "44"]).unwrap(),
opt
);
// Partial subcommand update
let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 }));
opt.try_update_from(&["test", "child1", "command1", "42"])
.unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "child1", "command1", "42", "14"]).unwrap(),
opt
);
// Partial subcommand update
let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 }));
opt.try_update_from(&["test", "child1", "command2", "43"])
.unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "child1", "command2", "43"]).unwrap(),
opt
);
// Change subcommand
let mut opt = Opt::Child1(Child1::Command1(Command1 { arg1: 12, arg2: 14 }));
opt.try_update_from(&["test", "child2", "command2", "43"])
.unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "child2", "command2", "43"]).unwrap(),
opt
);
}
#[test]
fn update_ext_subcommand() {
#[derive(Parser, PartialEq, Eq, Debug)]
enum Opt {
Command1(Command1),
Command2(Command2),
#[command(external_subcommand)]
Ext(Vec<String>),
}
#[derive(Args, PartialEq, Eq, Debug)]
struct Command1 {
arg1: i32,
arg2: i32,
}
#[derive(Args, PartialEq, Eq, Debug)]
struct Command2 {
arg2: i32,
}
// Full subcommand update
let mut opt = Opt::Ext(vec!["12".into(), "14".into()]);
opt.try_update_from(&["test", "ext", "42", "44"]).unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "ext", "42", "44"]).unwrap(),
opt
);
// No partial subcommand update
let mut opt = Opt::Ext(vec!["12".into(), "14".into()]);
opt.try_update_from(&["test", "ext", "42"]).unwrap();
assert_eq!(Opt::try_parse_from(&["test", "ext", "42"]).unwrap(), opt);
// Change subcommand
let mut opt = Opt::Ext(vec!["12".into(), "14".into()]);
opt.try_update_from(&["test", "command2", "43"]).unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "command2", "43"]).unwrap(),
opt
);
let mut opt = Opt::Command1(Command1 { arg1: 12, arg2: 14 });
opt.try_update_from(&["test", "ext", "42", "44"]).unwrap();
assert_eq!(
Opt::try_parse_from(&["test", "ext", "42", "44"]).unwrap(),
opt
);
}
#[test]
fn subcommand_name_not_literal() {
fn get_name() -> &'static str {
"renamed"
}
#[derive(Parser, PartialEq, Eq, Debug)]
struct Opt {
#[command(subcommand)]
subcmd: SubCmd,
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
enum SubCmd {
#[command(name = get_name())]
SubCmd1,
}
assert!(Opt::try_parse_from(&["test", "renamed"]).is_ok());
}
#[test]
fn skip_subcommand() {
#[derive(Debug, PartialEq, Eq, Parser)]
struct Opt {
#[command(subcommand)]
sub: Subcommands,
}
#[derive(Debug, PartialEq, Eq, Subcommand)]
enum Subcommands {
Add,
Remove,
#[allow(dead_code)]
#[command(skip)]
Skip,
}
assert_eq!(
Opt::try_parse_from(&["test", "add"]).unwrap(),
Opt {
sub: Subcommands::Add
}
);
assert_eq!(
Opt::try_parse_from(&["test", "remove"]).unwrap(),
Opt {
sub: Subcommands::Remove
}
);
let res = Opt::try_parse_from(&["test", "skip"]);
assert_eq!(
res.unwrap_err().kind(),
clap::error::ErrorKind::InvalidSubcommand,
);
}
#[test]
fn built_in_subcommand_escaped() {
#[derive(Debug, PartialEq, Eq, Parser)]
enum Command {
Install {
arg: Option<String>,
},
#[command(external_subcommand)]
Custom(Vec<String>),
}
assert_eq!(
Command::try_parse_from(&["test", "install", "arg"]).unwrap(),
Command::Install {
arg: Some(String::from("arg"))
}
);
assert_eq!(
Command::try_parse_from(&["test", "--", "install"]).unwrap(),
Command::Custom(vec![String::from("install")])
);
assert_eq!(
Command::try_parse_from(&["test", "--", "install", "arg"]).unwrap(),
Command::Custom(vec![String::from("install"), String::from("arg")])
);
}