use std::collections::BTreeMap;
use std::path::PathBuf;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use super::WrappedTool;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Tail {
#[default]
Deny,
AfterDashDash,
Forward,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Stdin {
#[default]
Closed,
Pipe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Style {
Separate,
Equals,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Flag {
pub(crate) name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) aliases: Vec<String>,
#[serde(default)]
pub(crate) takes_value: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) repeatable: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) required: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) int: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) choices: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) style: Option<Style>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub(crate) about: String,
}
impl Flag {
pub fn switch(name: impl Into<String>) -> Self {
Self {
name: name.into(),
aliases: Vec::new(),
takes_value: false,
repeatable: false,
required: false,
int: false,
choices: Vec::new(),
style: None,
about: String::new(),
}
}
pub fn value(name: impl Into<String>) -> Self {
Self {
takes_value: true,
..Self::switch(name)
}
}
pub fn alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
pub fn repeatable(mut self) -> Self {
self.repeatable = true;
self
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn int(mut self) -> Self {
self.int = true;
self
}
pub fn choices(mut self, choices: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.choices = choices.into_iter().map(Into::into).collect();
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn about(mut self, about: impl Into<String>) -> Self {
self.about = about.into();
self
}
pub(crate) fn written_name(&self) -> String {
written_form(&self.name)
}
pub(crate) fn effective_style(&self) -> Style {
self.style.unwrap_or({
if self.name.chars().count() == 1 {
Style::Separate
} else {
Style::Equals
}
})
}
pub(crate) fn spellings(&self) -> Vec<String> {
let mut spellings: Vec<String> = self.aliases.iter().map(|a| written_form(a)).collect();
spellings.push(self.written_name());
spellings
}
pub(crate) fn matches(&self, spelling: &str) -> bool {
self.written_name() == spelling
|| self.aliases.iter().any(|alias| written_form(alias) == spelling)
}
pub(crate) fn allowed_spelling(&self) -> String {
self.spellings().join("/")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Positional {
pub(crate) name: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) many: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) path_under: Option<PathBuf>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub(crate) about: String,
}
impl Positional {
pub fn one(name: impl Into<String>) -> Self {
Self {
name: name.into(),
many: false,
required: false,
path_under: None,
about: String::new(),
}
}
pub fn many(name: impl Into<String>) -> Self {
Self {
many: true,
..Self::one(name)
}
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn path_under(mut self, root: impl Into<PathBuf>) -> Self {
self.path_under = Some(root.into());
self
}
pub fn about(mut self, about: impl Into<String>) -> Self {
self.about = about.into();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Verb {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) lead: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) omit_name: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) flags: Vec<Flag>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) positionals: Vec<Positional>,
#[serde(default)]
pub(crate) tail: Tail,
#[serde(default)]
pub(crate) stdin: Stdin,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) json_output: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub(crate) about: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) examples: Vec<(String, String)>,
}
impl Verb {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::default()
}
}
pub fn root() -> Self {
Self::default()
}
pub fn lead(mut self, lead: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.lead = lead.into_iter().map(Into::into).collect();
self
}
pub fn omit_name(mut self) -> Self {
self.omit_name = true;
self
}
pub fn flag(mut self, flag: Flag) -> Self {
self.flags.push(flag);
self
}
pub fn positional(mut self, positional: Positional) -> Self {
self.positionals.push(positional);
self
}
pub fn tail(mut self, tail: Tail) -> Self {
self.tail = tail;
self
}
pub fn stdin(mut self, stdin: Stdin) -> Self {
self.stdin = stdin;
self
}
pub fn json_output(mut self) -> Self {
self.json_output = true;
self
}
pub fn about(mut self, about: impl Into<String>) -> Self {
self.about = about.into();
self
}
pub fn example(mut self, label: impl Into<String>, command: impl Into<String>) -> Self {
self.examples.push((label.into(), command.into()));
self
}
pub(crate) fn name_or_root(&self) -> &str {
self.name.as_deref().unwrap_or("")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct WrappedCommand {
pub(crate) name: String,
#[serde(default)]
pub(crate) executable: PathBuf,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub(crate) about: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) lead: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) env: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) root: Option<Verb>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) verbs: Vec<Verb>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) examples: Vec<(String, String)>,
}
impl WrappedCommand {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Self::default()
}
}
pub fn executable(mut self, executable: impl Into<PathBuf>) -> Self {
self.executable = executable.into();
self
}
pub fn about(mut self, about: impl Into<String>) -> Self {
self.about = about.into();
self
}
pub fn lead(mut self, lead: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.lead = lead.into_iter().map(Into::into).collect();
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn root(mut self, root: Verb) -> Self {
self.root = Some(root);
self
}
pub fn verb(mut self, verb: Verb) -> Self {
self.verbs.push(verb);
self
}
pub fn example(mut self, label: impl Into<String>, command: impl Into<String>) -> Self {
self.examples.push((label.into(), command.into()));
self
}
pub fn build(self) -> Result<WrappedTool> {
self.check_shape()?;
let executable = self.check_executable()?;
Ok(WrappedTool::from_parts(self, executable))
}
fn check_shape(&self) -> Result<()> {
if self.name.is_empty() {
bail!("a wrapped command needs a name");
}
if self.root.is_none() && self.verbs.is_empty() {
bail!(
"wrapped command '{}' declares no verbs and no root, so it can accept no call",
self.name
);
}
if let Some(root) = &self.root
&& root.name.is_some()
{
bail!(
"wrapped command '{}' passed a named verb to root(); use Verb::root()",
self.name
);
}
let mut seen: Vec<&str> = Vec::new();
for verb in &self.verbs {
let Some(name) = verb.name.as_deref() else {
bail!(
"wrapped command '{}' passed an unnamed verb to verb(); use root()",
self.name
);
};
if name.is_empty() {
bail!("wrapped command '{}' declares a verb with no name", self.name);
}
if seen.contains(&name) {
bail!(
"wrapped command '{}' declares the verb '{name}' twice",
self.name
);
}
seen.push(name);
}
for verb in self.root.iter().chain(self.verbs.iter()) {
self.check_verb(verb)?;
}
Ok(())
}
fn check_verb(&self, verb: &Verb) -> Result<()> {
let scope = self.scope_of(verb);
let mut spellings: Vec<String> = Vec::new();
for flag in &verb.flags {
if flag.name.is_empty() {
bail!("'{scope}' declares a flag with no name");
}
if !flag.takes_value && !flag.choices.is_empty() {
bail!(
"'{scope}' declares choices on the switch '{}'; a switch binds no value",
flag.written_name()
);
}
if !flag.takes_value && flag.int {
bail!(
"'{scope}' declares int on the switch '{}'; a switch binds no value",
flag.written_name()
);
}
for spelling in flag.spellings() {
if spellings.contains(&spelling) {
bail!("'{scope}' declares the flag spelling '{spelling}' twice");
}
spellings.push(spelling);
}
}
let mut seen_optional: Option<&str> = None;
for (slot, positional) in verb.positionals.iter().enumerate() {
if positional.name.is_empty() {
bail!("'{scope}' declares a positional with no name");
}
if verb.positionals[..slot]
.iter()
.any(|p| p.name == positional.name)
{
bail!(
"'{scope}' declares the positional '{}' twice",
positional.name
);
}
if positional.many && slot + 1 != verb.positionals.len() {
bail!(
"'{scope}' declares the many positional '{}' before '{}'; a many \
positional absorbs the rest, so it must be last",
positional.name,
verb.positionals[slot + 1].name
);
}
match (positional.required, seen_optional) {
(true, Some(optional)) => bail!(
"'{scope}' declares the required positional '{}' after the optional \
'{optional}'; no call could fill '{optional}' without it",
positional.name
),
(false, None) => seen_optional = Some(&positional.name),
_ => {}
}
if let Some(root) = &positional.path_under
&& !root.is_absolute()
{
bail!(
"'{scope}' declares path_under({}) on '{}'; the root must be an \
absolute path",
root.display(),
positional.name
);
}
}
Ok(())
}
fn check_executable(&self) -> Result<PathBuf> {
let path = &self.executable;
if path.as_os_str().is_empty() {
bail!("wrapped command '{}' declares no executable", self.name);
}
if !path.is_absolute() {
bail!(
"wrapped command '{}': executable {} is not an absolute path",
self.name,
path.display()
);
}
let metadata = std::fs::metadata(path).map_err(|e| {
anyhow::anyhow!(
"wrapped command '{}': executable {} cannot be read ({e})",
self.name,
path.display()
)
})?;
if !metadata.is_file() {
bail!(
"wrapped command '{}': executable {} is not a file",
self.name,
path.display()
);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 == 0 {
bail!(
"wrapped command '{}': executable {} has no execute bit",
self.name,
path.display()
);
}
}
Ok(path.clone())
}
pub(crate) fn scope_of(&self, verb: &Verb) -> String {
match &verb.name {
Some(name) => format!("{} {name}", self.name),
None => self.name.clone(),
}
}
}
pub(crate) fn written_form(name: &str) -> String {
if name.starts_with('-') {
name.to_string()
} else if name.chars().count() == 1 {
format!("-{name}")
} else {
format!("--{name}")
}
}
pub fn find_executable(name: &str, path_var: &str) -> Option<PathBuf> {
crate::tools::resolve_in_path(name, path_var).map(PathBuf::from)
}