use std::ffi::OsString;
use std::path::Path;
use shep_core::dogs::manifest::{Subcommand, Verb};
use shep_core::paths::ShepPaths;
use crate::cli::HELP_TEMPLATE;
use crate::commands::dogs::{cached_manifest, collides_with_a_verb};
use crate::commands::shep_toml::ShepToml;
use crate::exit::ExitCode;
use crate::{cli, home};
const DOG_GROUP_HEADING: &str = "Dogs and agents";
const VERB_COLUMN: usize = 17;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct AdoptedVerb {
pub name: String,
pub description: Option<String>,
pub subcommands: Vec<Subcommand>,
}
#[must_use]
pub(crate) fn adopted_verbs(paths: &ShepPaths) -> Vec<AdoptedVerb> {
let Ok(config) = ShepToml::read_only(&paths.daemon_config) else {
return Vec::new();
};
let mut verbs: Vec<AdoptedVerb> = config
.adopted_dog_names()
.into_iter()
.filter(|name| !collides_with_a_verb(name))
.filter(|name| printable(name) == *name)
.map(|name| {
let verb = config
.adopted_dog_path(&name)
.as_deref()
.and_then(|path| cached_manifest(path, &paths.home))
.and_then(|manifest| manifest.verb)
.and_then(printable_verb);
AdoptedVerb {
description: verb
.as_ref()
.map(|verb| verb.description.clone())
.filter(|description| !description.is_empty()),
subcommands: verb.map_or_else(Vec::new, |verb| verb.subcommands),
name,
}
})
.collect();
verbs.sort_by(|a, b| a.name.cmp(&b.name));
verbs
}
fn printable(field: &str) -> String {
let (cleaned, _) = crate::terminal_safe::sanitise(field);
cleaned.replace(['{', '}'], "")
}
fn printable_verb(verb: Verb) -> Option<Verb> {
let mut cleaned = Verb::new(printable(&verb.description));
cleaned.subcommands = verb
.subcommands
.into_iter()
.filter_map(|sub| {
let name = printable(&sub.name);
if name.is_empty() || name.chars().any(char::is_whitespace) {
return None;
}
Some(Subcommand::new(name, printable(&sub.about)))
})
.collect();
if cleaned.description.is_empty() && cleaned.subcommands.is_empty() {
return None;
}
Some(cleaned)
}
#[must_use]
pub(crate) fn top_level_help_asked(argv: &[OsString]) -> bool {
argv.len() == 1
|| argv[1..]
.iter()
.any(|arg| arg == "-h" || arg == "--help" || arg == "help")
}
pub(crate) fn paths_from_argv(argv: &[OsString]) -> Option<ShepPaths> {
let global = cli::GlobalArgs {
host: None,
home: crate::adopted::home_before(&argv[1..]),
format: cli::Format::Table,
quiet: false,
style: None,
};
home::resolve_paths(&global).ok()
}
#[track_caller]
pub(crate) fn list_in_help(command: &mut clap::Command, verbs: &[AdoptedVerb]) {
if verbs.is_empty() {
return;
}
let mut lines: Vec<String> = HELP_TEMPLATE.lines().map(str::to_string).collect();
let at = lines
.iter()
.position(|line| line.starts_with(DOG_GROUP_HEADING))
.expect("HELP_TEMPLATE has a Dogs group");
lines.splice(at + 1..at + 1, dog_lines(verbs));
*command = std::mem::take(command).help_template(lines.join("\n"));
}
fn dog_lines(verbs: &[AdoptedVerb]) -> Vec<String> {
let widest = verbs
.iter()
.map(|verb| crate::output::width::text_columns(&verb.name))
.max();
verbs
.iter()
.map(|verb| {
let mut line = " ".repeat(VERB_COLUMN);
line.push_str(&verb.name);
if let Some(description) = &verb.description {
let name_columns = crate::output::width::text_columns(&verb.name);
let pad = widest.unwrap_or(0) + 2 - name_columns;
line.push_str(&" ".repeat(pad));
line.push_str(description);
}
line
})
.collect()
}
fn shell_safe(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
first.is_alphanumeric() && chars.all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.'))
}
pub(crate) fn with_completion_entries(
command: clap::Command,
verbs: &[AdoptedVerb],
) -> clap::Command {
verbs
.iter()
.filter(|verb| shell_safe(&verb.name))
.fold(command, |command, verb| {
command.subcommand(completion_entry(verb))
})
}
fn completion_entry(verb: &AdoptedVerb) -> clap::Command {
let mut command = clap::Command::new(verb.name.clone())
.about(verb.description.clone().unwrap_or_default())
.disable_help_subcommand(true);
for sub in &verb.subcommands {
if !shell_safe(&sub.name) {
continue;
}
command = command.subcommand(clap::Command::new(sub.name.clone()).about(sub.about.clone()));
}
command
}
pub(crate) fn describe_dog(
name: &str,
path: &Path,
home: &Path,
argv: &[OsString],
) -> Option<ExitCode> {
if !matches!(argv, [flag] if flag == "--help" || flag == "-h") {
return None;
}
let verb = cached_manifest(path, home)
.and_then(|manifest| manifest.verb)
.and_then(printable_verb)?;
if verb.subcommands.is_empty() {
return None;
}
let _ = dog_help(name, &verb).print_long_help();
Some(ExitCode::Success)
}
fn dog_help(name: &str, verb: &Verb) -> clap::Command {
let command = clap::Command::new(name.to_owned())
.bin_name(format!("shep {name}"))
.disable_help_subcommand(true)
.after_help(format!(
"Anything else after `shep {name}` goes to the dog unchanged."
));
let command = if verb.description.is_empty() {
command
} else {
command.about(verb.description.clone())
};
verb.subcommands.iter().fold(command, |command, sub| {
command.subcommand(clap::Command::new(sub.name.clone()).about(sub.about.clone()))
})
}
#[cfg(test)]
pub(crate) fn dog_help_text(name: &str, verb: &Verb) -> String {
dog_help(name, verb).render_long_help().to_string()
}
#[cfg(all(test, unix))]
mod tests;