use std::ffi::OsString;
use std::path::Path;
use shep_core::dogs::manifest::{MAX_EXAMPLES, Subcommand, Verb};
use shep_core::paths::ShepPaths;
use crate::cli::{DogLine, top_level_template};
use crate::commands::dogs::{cached_manifest, collides_with_a_verb, installed};
use crate::commands::shep_toml::ShepToml;
use crate::dog_index::{self, Index};
use crate::exit::ExitCode;
use crate::{cli, home};
#[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> {
adopted_verbs_in(paths, &dog_index::index_url())
}
fn adopted_verbs_in(paths: &ShepPaths, index_url: &str) -> 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();
if verbs.iter().any(|verb| verb.description.is_none())
&& let Some(index) = dog_index::cached_only(paths, index_url)
{
for verb in verbs.iter_mut().filter(|verb| verb.description.is_none()) {
let receipt = installed(paths, &verb.name);
let package = receipt.as_ref().map(|receipt| receipt.package.as_str());
verb.description = indexed_description(&index, &verb.name, package);
}
}
verbs.sort_by(|a, b| a.name.cmp(&b.name));
verbs
}
fn indexed_description(index: &Index, name: &str, package: Option<&str>) -> Option<String> {
let row = match package {
Some(package) => index.dogs.iter().find(|dog| dog.package == package)?,
None => index.dogs.iter().find(|dog| dog.adopt_as == name)?,
};
Some(printable(&row.description)).filter(|description| !description.is_empty())
}
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;
}
let cleaned = Subcommand::new(name, printable(&sub.about));
Some(match sub.usage.map(|usage| printable(&usage)) {
Some(usage) if !usage.is_empty() => cleaned.with_usage(usage),
_ => cleaned,
})
})
.collect();
cleaned.examples = verb
.examples
.iter()
.map(|example| printable(example))
.filter(|example| !example.is_empty())
.take(MAX_EXAMPLES)
.collect();
if cleaned.description.is_empty()
&& cleaned.subcommands.is_empty()
&& cleaned.examples.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()
}
pub(crate) fn list_in_help(command: &mut clap::Command, verbs: &[AdoptedVerb], deep: bool) {
let dogs: Vec<DogLine<'_>> = verbs
.iter()
.map(|verb| DogLine {
name: &verb.name,
about: verb.description.as_deref(),
})
.collect();
let template = top_level_template(command, &dogs, deep);
*command = std::mem::take(command).help_template(template);
}
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,
paths: &ShepPaths,
before: &[OsString],
argv: &[OsString],
) -> Option<ExitCode> {
let mut page = asked_page(name, path, paths, &dog_index::index_url(), before, argv)?;
let _ = page.print_long_help();
Some(ExitCode::Success)
}
fn asked_page(
name: &str,
path: &Path,
paths: &ShepPaths,
index_url: &str,
before: &[OsString],
argv: &[OsString],
) -> Option<clap::Command> {
help_asked(before, argv).then(|| dog_page(name, path, paths, index_url))
}
fn help_asked(before: &[OsString], argv: &[OsString]) -> bool {
match argv {
[flag] => flag == "--help" || flag == "-h",
[] => help_verb_precedes(before),
_ => false,
}
}
fn help_verb_precedes(before: &[OsString]) -> bool {
let mut words = Vec::new();
let mut tokens = before.iter();
while let Some(token) = tokens.next() {
if ["--home", "--host", "--format", "--style"]
.iter()
.any(|flag| token == flag)
{
tokens.next();
} else if !token.to_string_lossy().starts_with('-') {
words.push(token);
}
}
matches!(words.as_slice(), [word] if *word == "help")
}
fn dog_page(name: &str, path: &Path, paths: &ShepPaths, index_url: &str) -> clap::Command {
let manifest = cached_manifest(path, &paths.home);
let receipt = installed(paths, name);
let running = manifest.as_ref().map(|manifest| manifest.version.as_str());
let recorded = receipt
.as_ref()
.and_then(|receipt| receipt.version.as_deref());
let version = running.or(recorded).map(printable);
let source = receipt
.as_ref()
.filter(|_| running.is_none() || recorded.is_none() || running == recorded)
.map(|receipt| printable(&receipt.source()));
let verb = manifest
.and_then(|manifest| manifest.verb)
.and_then(printable_verb);
match verb {
Some(verb) => {
let who = [
Some(name.to_owned()),
version,
source.map(|source| format!("from {source}")),
];
let who = who.into_iter().flatten().collect::<Vec<_>>().join(" ");
dog_help(
name,
&verb,
&format!("{who}. Anything after `shep {name}` goes to the dog."),
)
}
None => {
let description = dog_index::cached_only(paths, index_url).and_then(|index| {
let package = receipt.as_ref().map(|receipt| receipt.package.as_str());
indexed_description(&index, name, package)
});
stub(name, version.as_deref(), description.as_deref())
}
}
}
fn stub(name: &str, version: Option<&str>, description: Option<&str>) -> clap::Command {
let who = version.map_or_else(|| name.to_owned(), |version| format!("{name} {version}"));
let described = description.map_or_else(String::new, |text| format!("\n{text}"));
let text = format!(
"{who}{described}\n\nThis dog declares no help. `shep {name} help` asks the dog itself."
);
clap::Command::new(name.to_owned())
.disable_help_flag(true)
.help_template("{about}")
.about(text)
}
fn dog_help(name: &str, verb: &Verb, closing: &str) -> clap::Command {
let usage = if verb.subcommands.is_empty() {
format!("shep {name} [args]")
} else {
format!("shep {name} <command> [args]")
};
let examples = shep_client::dogs::render_examples(&verb.examples);
let after = if examples.is_empty() {
closing.to_owned()
} else {
format!("{examples}\n\n{closing}")
};
let command = clap::Command::new(name.to_owned())
.bin_name(format!("shep {name}"))
.override_usage(usage)
.disable_help_subcommand(true)
.disable_help_flag(true)
.after_help(after);
let command = if verb.description.is_empty() {
command
} else {
command.about(verb.description.clone())
};
verb.subcommands.iter().fold(command, |command, sub| {
let label = match &sub.usage {
Some(usage) => format!("{} {usage}", sub.name),
None => sub.name.clone(),
};
command.subcommand(clap::Command::new(label).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;