use std::ffi::OsString;
use std::io::IsTerminal;
use clap::{CommandFactory, FromArgMatches};
use cli::Cli;
use crate::adopted::dispatch_adopted_dog;
use crate::adopted_help;
use crate::config::{help_colour_choice, must_render_bare, resolve_style};
use crate::dispatch::run;
use crate::home::print_shepherd_status;
use crate::{cli, exit, lookout, output, style};
use exit::ExitCode;
pub(crate) fn alias_argv(verb: &str, mut argv: Vec<OsString>) -> Vec<OsString> {
let passthrough = matches!(
argv.get(1).and_then(|arg| arg.to_str()),
Some("daemon" | "dog")
);
if !passthrough {
argv.insert(1, OsString::from(verb));
}
argv
}
pub(crate) fn run_argv(argv: Vec<OsString>) -> std::process::ExitCode {
if std::env::var_os("SHEP_TERM_PANIC_PROBE").is_some() {
lookout::term::probe_panic_for_test();
}
let deep = style::deep_colour_terminal(
std::env::var_os("TERM").as_deref(),
std::env::var_os("COLORTERM").as_deref(),
);
let mut command = Cli::command()
.styles(output::paint::help_styles(deep))
.color(help_colour_choice(
&argv,
std::env::var("SHEP_STYLE").ok().as_deref(),
));
let dogs = if adopted_help::top_level_help_asked(&argv)
&& let Some(paths) = adopted_help::paths_from_argv(&argv)
{
adopted_help::adopted_verbs(&paths)
} else {
Vec::new()
};
adopted_help::list_in_help(&mut command, &dogs, deep);
let parsed = command
.try_get_matches_from(argv.clone())
.and_then(|mut matches| Cli::from_arg_matches_mut(&mut matches));
if let Err(ref err) = parsed
&& let Some(code) = dispatch_adopted_dog(&argv, err)
{
return code;
}
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(err) => {
eprintln!("shep: could not start an async runtime: {err}");
return ExitCode::Failure.into();
}
};
let cli = match parsed {
Ok(cli) => cli,
Err(err) => {
#[cfg(unix)]
if matches!(
err.kind(),
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::MissingSubcommand
) {
runtime.block_on(print_shepherd_status(&argv));
}
if let Some(text) = usage_error_text(&err) {
let _ = std::io::Write::write_all(&mut std::io::stderr(), text.as_bytes());
std::process::exit(err.exit_code());
}
err.exit();
}
};
let (configured, style_source) = resolve_style(&cli.global);
let level = if must_render_bare(std::io::stdout().is_terminal(), cli.global.format) {
style::StyleLevel::Bare
} else {
configured
};
let style = style::Presentation::new(
level,
std::env::var_os("NO_COLOR").as_deref(),
std::env::var_os("TERM").as_deref(),
std::env::var_os("COLORTERM").as_deref(),
output::terminal_width(),
);
runtime
.block_on(run(cli, style, (configured, style_source)))
.into()
}
fn usage_error_text(err: &clap::Error) -> Option<String> {
use clap::error::ErrorKind;
let is_help = matches!(
err.kind(),
ErrorKind::DisplayHelp
| ErrorKind::DisplayVersion
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
(!is_help).then(|| err.render().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn a_usage_error_prints_no_escape_codes_while_the_help_is_coloured() {
for argv in [["shep", "strt"], ["shep", "--nonsense"]] {
let err = coloured(Cli::command())
.try_get_matches_from(argv)
.unwrap_err();
assert_plain_with_usage(&err, &format!("{argv:?}"));
}
}
#[test]
fn a_missing_subcommand_error_prints_no_escape_codes_too() {
let command = clap::Command::new("shep")
.subcommand_required(true)
.subcommand(clap::Command::new("flock"));
let err = coloured(command)
.try_get_matches_from(["shep"])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingSubcommand);
assert_plain_with_usage(&err, "missing subcommand");
}
fn coloured(command: clap::Command) -> clap::Command {
command
.styles(output::paint::help_styles(true))
.color(clap::ColorChoice::Always)
}
fn assert_plain_with_usage(err: &clap::Error, what: &str) {
assert!(
err.render().ansi().to_string().contains('\x1b'),
"{what}: clap's own rendering must be coloured"
);
let text = usage_error_text(err)
.unwrap_or_else(|| panic!("{what}: kind {:?} is not a usage error", err.kind()));
assert!(!text.contains('\x1b'), "{what}: got {text:?}");
assert!(text.contains("Usage:"), "{what}: got {text:?}");
}
#[test]
fn help_and_version_are_left_to_clap() {
for argv in [["shep", "--help"], ["shep", "--version"]] {
let err = Cli::command().try_get_matches_from(argv).unwrap_err();
assert_eq!(usage_error_text(&err), None, "{argv:?}");
}
}
#[test]
fn an_alias_supplies_its_verb() {
let argv = alias_argv(
"runtime",
vec!["shep-runtime".into(), "./Flockfile.toml".into()],
);
assert_eq!(
argv,
vec![
OsString::from("shep-runtime"),
OsString::from("runtime"),
OsString::from("./Flockfile.toml"),
]
);
}
#[test]
fn an_alias_with_no_arguments_still_supplies_its_verb() {
let argv = alias_argv("dev", vec!["shep-dev".into()]);
assert_eq!(
argv,
vec![OsString::from("shep-dev"), OsString::from("dev")]
);
}
#[test]
fn an_alias_passes_the_two_re_exec_verbs_through_untouched() {
for verb in ["daemon", "dog"] {
let argv = alias_argv(
"runtime",
vec!["shep-runtime".into(), verb.into(), "metrics".into()],
);
assert_eq!(
argv[1],
OsString::from(verb),
"{verb} must not be rewritten"
);
assert_eq!(argv.len(), 3, "{verb}: nothing may be inserted");
}
}
#[test]
fn the_pass_through_matches_the_whole_argument_and_not_a_prefix() {
let argv = alias_argv("runtime", vec!["shep-runtime".into(), "dogfood".into()]);
assert_eq!(argv[1], OsString::from("runtime"));
assert_eq!(argv[2], OsString::from("dogfood"));
}
#[test]
fn the_alias_vector_parses_to_the_expected_command() {
use clap::Parser;
use cli::Commands;
let argv = alias_argv(
"dog",
vec!["shep-runtime".into(), "dog".into(), "metrics".into()],
);
let cli = Cli::try_parse_from(argv).expect("the passthrough vector must parse");
assert!(matches!(cli.command, Commands::Dog(_)));
}
#[test]
fn the_runtime_alias_vector_parses_to_the_runtime_command() {
use clap::Parser;
use cli::Commands;
let argv = alias_argv("runtime", vec!["shep-runtime".into(), "--supervise".into()]);
let cli = Cli::try_parse_from(argv).unwrap();
let Commands::Runtime(args) = cli.command else {
panic!("expected runtime")
};
assert!(args.supervise);
}
#[test]
fn a_subcommand_answers_version() {
use clap::Parser;
let err = Cli::try_parse_from(["shep", "dogs", "--version"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
}
#[test]
fn save_parses_to_its_own_command() {
use clap::Parser;
use cli::Commands;
assert!(matches!(
Cli::try_parse_from(["shep", "save"]).unwrap().command,
Commands::Save
));
}
#[test]
fn dogs_parses_to_its_own_command() {
use clap::Parser;
use cli::Commands;
assert!(matches!(
Cli::try_parse_from(["shep", "dogs"]).unwrap().command,
Commands::Dogs(_)
));
}
#[test]
fn dogs_available_parses_with_its_filter() {
use clap::Parser;
use cli::Commands;
let parsed = Cli::try_parse_from(["shep", "dogs", "--available", "spot"])
.unwrap()
.command;
let Commands::Dogs(args) = parsed else {
panic!("expected dogs")
};
assert!(args.available);
assert_eq!(args.filter.as_deref(), Some("spot"));
}
#[test]
fn enable_and_disable_parse_to_their_own_commands_and_require_a_name() {
use clap::Parser;
use cli::Commands;
let enabled = Cli::try_parse_from(["shep", "enable", "metrics"])
.unwrap()
.command;
let Commands::Enable(args) = enabled else {
panic!("expected enable")
};
assert_eq!(args.name, "metrics");
let disabled = Cli::try_parse_from(["shep", "disable", "metrics"])
.unwrap()
.command;
let Commands::Disable(args) = disabled else {
panic!("expected disable")
};
assert_eq!(args.name, "metrics");
assert!(
Cli::try_parse_from(["shep", "enable"]).is_err(),
"`shep enable` with no name must be a usage error"
);
assert!(
Cli::try_parse_from(["shep", "disable"]).is_err(),
"`shep disable` with no name must be a usage error"
);
}
#[test]
fn adopt_and_rehome_parse_to_their_own_commands_and_require_their_arguments() {
use clap::Parser;
use cli::Commands;
let adopted =
Cli::try_parse_from(["shep", "adopt", "/opt/bin/shep-otel", "--name", "otel"])
.unwrap()
.command;
let Commands::Adopt(args) = adopted else {
panic!("expected adopt")
};
assert_eq!(args.path, PathBuf::from("/opt/bin/shep-otel"));
assert_eq!(args.name, Some("otel".to_string()));
let unnamed = Cli::try_parse_from(["shep", "adopt", "/opt/bin/shep-otel"])
.unwrap()
.command;
let Commands::Adopt(args) = unnamed else {
panic!("expected adopt")
};
assert_eq!(args.path, PathBuf::from("/opt/bin/shep-otel"));
assert_eq!(args.name, None);
let rehomed = Cli::try_parse_from(["shep", "rehome", "otel"])
.unwrap()
.command;
let Commands::Rehome(args) = rehomed else {
panic!("expected rehome")
};
assert_eq!(args.name, "otel");
assert!(
Cli::try_parse_from(["shep", "adopt"]).is_err(),
"`shep adopt` with no path must be a usage error"
);
assert!(
Cli::try_parse_from(["shep", "rehome"]).is_err(),
"`shep rehome` with no name must be a usage error"
);
}
#[test]
fn the_hidden_pm2_spelling_reaches_adopt_with_the_arguments_the_right_way_round() {
use clap::Parser;
use cli::Commands;
let parsed = Cli::try_parse_from(["shep", "enable", "--exec", "/opt/bin/d", "otel"])
.unwrap()
.command;
let Commands::Enable(args) = parsed else {
panic!("expected enable")
};
assert_eq!(args.name, "otel");
assert_eq!(args.exec, Some(PathBuf::from("/opt/bin/d")));
let plain = Cli::try_parse_from(["shep", "enable", "metrics"])
.unwrap()
.command;
let Commands::Enable(args) = plain else {
panic!("expected enable")
};
assert_eq!(args.exec, None);
}
#[test]
fn muster_parses_to_its_own_command() {
use clap::Parser;
use cli::Commands;
assert!(matches!(
Cli::try_parse_from(["shep", "muster"]).unwrap().command,
Commands::Muster
));
}
#[test]
fn import_pm2_parses_to_its_own_subcommand() {
use clap::Parser;
use cli::{Commands, ImportCommand};
let cli = Cli::try_parse_from(["shep", "import", "pm2"]).unwrap();
let Commands::Import(args) = cli.command else {
panic!("`shep import pm2` did not reach the import verb");
};
assert!(matches!(args.command, ImportCommand::Pm2(_)));
}
#[test]
fn import_env_parses_its_file_app_and_secret_patterns() {
use clap::Parser;
use cli::{Commands, ImportCommand};
let cli = Cli::try_parse_from([
"shep", "import", "env", "app.env", "--app", "web", "--secret", "DB_*",
])
.unwrap();
let Commands::Import(args) = cli.command else {
panic!("`shep import env` did not reach the import verb");
};
let ImportCommand::Env(args) = args.command else {
panic!("`shep import env` did not reach its own subcommand");
};
assert_eq!(args.file.to_str(), Some("app.env"));
assert_eq!(args.app, "web");
assert_eq!(args.secret, ["DB_*"]);
}
#[test]
fn bare_import_no_longer_parses() {
use clap::Parser;
assert!(
Cli::try_parse_from(["shep", "import"]).is_err(),
"bare `shep import` must name a subcommand"
);
}
#[test]
fn barks_parses_to_its_own_command() {
use clap::Parser;
use cli::Commands;
assert!(matches!(
Cli::try_parse_from(["shep", "barks"]).unwrap().command,
Commands::Barks(_)
));
}
#[test]
fn startup_and_unstartup_parse_to_their_own_commands() {
use clap::Parser;
use cli::Commands;
assert!(matches!(
Cli::try_parse_from(["shep", "startup"]).unwrap().command,
Commands::Startup(_)
));
assert!(matches!(
Cli::try_parse_from(["shep", "unstartup"]).unwrap().command,
Commands::Unstartup(_)
));
let named = Cli::try_parse_from(["shep", "startup", "--user", "deploy"])
.unwrap()
.command;
let Commands::Startup(args) = named else {
panic!("expected startup")
};
assert_eq!(args.user.as_deref(), Some("deploy"));
}
#[test]
fn resurrect_is_a_hidden_alias_for_muster() {
use clap::{CommandFactory, Parser};
use cli::Commands;
assert!(matches!(
Cli::try_parse_from(["shep", "resurrect"]).unwrap().command,
Commands::Muster
));
let cmd = Cli::command();
let muster = cmd.find_subcommand("muster").unwrap();
assert!(
muster.get_visible_aliases().next().is_none(),
"resurrect must stay out of --help"
);
}
}