use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(name = "viz-shell", version, arg_required_else_help = true)]
pub struct Cli {
#[command(subcommand)]
pub action: Option<Action>,
#[arg(short = 'c', long, value_name = "NAME", global = true)]
pub config: Option<String>,
#[arg(short = 'f', long = "file", value_name = "FILE", global = true)]
pub files: Vec<PathBuf>,
#[arg(long, global = true)]
pub show_effective_config: bool,
#[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", global = true)]
pub env: Vec<String>,
#[arg(long, global = true)]
pub show_env: bool,
#[arg(last = true)]
pub command: Vec<String>,
}
#[derive(Debug, PartialEq, Subcommand)]
pub enum Action {
New {
name: Option<String>,
#[arg(last = true)]
command: Vec<String>,
},
#[command(visible_alias = "at")]
Attach {
target: Option<String>,
#[arg(last = true)]
command: Vec<String>,
},
Ls {
#[arg(long)]
all: bool,
},
Kill {
#[arg(required_unless_present = "all")]
targets: Vec<String>,
#[arg(long, conflicts_with = "targets")]
all: bool,
},
Configs,
#[command(hide = true)]
Entrypoint {
#[arg(long, conflicts_with = "command")]
hold: bool,
#[arg(last = true)]
command: Vec<String>,
},
#[command(hide = true)]
Enter {
#[arg(last = true)]
command: Vec<String>,
},
#[command(hide = true)]
AsUser {
#[arg(last = true, required = true)]
command: Vec<String>,
},
}
#[cfg(test)]
#[allow(non_snake_case)] mod tests {
use super::*;
fn strings(args: &[&str]) -> Vec<String> {
args.iter().map(|arg| arg.to_string()).collect()
}
#[test]
fn parse__nothing__help_instead() {
let error = Cli::try_parse_from(["vz"]).unwrap_err();
assert_eq!(
error.kind(),
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
fn parse__file_with_configs__before_or_after_it() {
for args in [
["vz", "-f", "other.vz.yml", "configs"],
["vz", "configs", "-f", "other.vz.yml"],
] {
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.files, [PathBuf::from("other.vz.yml")], "{args:?}");
}
}
#[test]
fn parse__file_repeated__each_in_order_short_or_long() {
let cli = Cli::try_parse_from(["vz", "-f", "app.vz.yml", "--file", "ci.yml"]).unwrap();
assert_eq!(
cli.files,
[PathBuf::from("app.vz.yml"), PathBuf::from("ci.yml")]
);
}
#[test]
fn parse__config_short_or_long__names_it() {
for args in [
["vz", "-c", "ci", "--", "true"],
["vz", "--config", "ci", "--", "true"],
] {
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.config.as_deref(), Some("ci"), "{args:?}");
}
}
#[test]
fn parse__show_flags_after_a_subcommand__apply() {
let cli =
Cli::try_parse_from(["vz", "new", "-c", "ci", "--show-effective-config"]).unwrap();
assert!(cli.show_effective_config);
}
#[test]
fn parse__env_repeated__kept_in_order() {
let cli = Cli::try_parse_from(["vz", "-e", "A=1", "--env", "GH_TOKEN"]).unwrap();
assert_eq!(cli.env, strings(&["A=1", "GH_TOKEN"]));
}
#[test]
fn parse__command_after_dashes__is_command() {
let cli = Cli::try_parse_from(["vz", "--", "id", "-u"]).unwrap();
assert_eq!(cli.command, strings(&["id", "-u"]));
}
#[test]
fn parse__entrypoint__carries_its_command() {
let cli = Cli::try_parse_from(["vz", "entrypoint", "--", "bash", "-l"]).unwrap();
let expected = Action::Entrypoint {
hold: false,
command: strings(&["bash", "-l"]),
};
assert_eq!(cli.action, Some(expected));
}
#[test]
fn parse__sessions__each_command() {
let cases: [(&[&str], Action); 10] = [
(
&["vz", "new"],
Action::New {
name: None,
command: vec![],
},
),
(
&["vz", "new", "--", "id"],
Action::New {
name: None,
command: strings(&["id"]),
},
),
(
&["vz", "new", "api"],
Action::New {
name: Some("api".to_owned()),
command: vec![],
},
),
(
&["vz", "new", "api", "--", "cargo", "test"],
Action::New {
name: Some("api".to_owned()),
command: strings(&["cargo", "test"]),
},
),
(
&["vz", "attach"],
Action::Attach {
target: None,
command: vec![],
},
),
(
&["vz", "at", "1", "--", "id"],
Action::Attach {
target: Some("1".to_owned()),
command: strings(&["id"]),
},
),
(&["vz", "ls", "--all"], Action::Ls { all: true }),
(&["vz", "configs"], Action::Configs),
(
&["vz", "kill", "0", "api"],
Action::Kill {
targets: strings(&["0", "api"]),
all: false,
},
),
(
&["vz", "kill", "--all"],
Action::Kill {
targets: vec![],
all: true,
},
),
];
for (args, expected) in cases {
let cli = Cli::try_parse_from(args).unwrap();
assert_eq!(cli.action, Some(expected), "{args:?}");
}
}
#[test]
fn parse__kill_without_targets__refused() {
let error = Cli::try_parse_from(["vz", "kill"]).unwrap_err();
assert_eq!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}
#[test]
fn parse__config_after_the_subcommand__applies() {
let cli = Cli::try_parse_from(["vz", "attach", "0", "-c", "trusted"]).unwrap();
assert_eq!(cli.config.as_deref(), Some("trusted"));
}
}