use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_with::{formats::PreferMany, serde_as, OneOrMany};
use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
mod error;
mod executable_task;
mod task_environment;
mod task_graph;
pub use executable_task::{
ExecutableTask, FailedToParseShellScript, InvalidWorkingDirectory, RunOutput,
TaskExecutionError,
};
pub use task_environment::{
AmbiguousTask, FindTaskError, FindTaskSource, SearchEnvironments, TaskAndEnvironment,
TaskDisambiguation,
};
pub use task_graph::{TaskGraph, TaskGraphError, TaskId, TaskNode};
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct TaskName(String);
impl TaskName {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn fancy_display(&self) -> console::StyledObject<&str> {
console::style(self.as_str()).blue()
}
}
impl From<&str> for TaskName {
fn from(name: &str) -> Self {
TaskName(name.to_string())
}
}
impl From<String> for TaskName {
fn from(name: String) -> Self {
TaskName(name)
}
}
impl From<TaskName> for String {
fn from(task_name: TaskName) -> Self {
task_name.0 }
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Task {
Plain(String),
Execute(Execute),
Alias(Alias),
#[serde(skip)]
Custom(Custom),
}
impl Task {
pub fn depends_on(&self) -> &[TaskName] {
match self {
Task::Plain(_) | Task::Custom(_) => &[],
Task::Execute(cmd) => &cmd.depends_on,
Task::Alias(cmd) => &cmd.depends_on,
}
}
pub fn as_plain(&self) -> Option<&String> {
match self {
Task::Plain(str) => Some(str),
_ => None,
}
}
pub fn as_execute(&self) -> Option<&Execute> {
match self {
Task::Execute(execute) => Some(execute),
_ => None,
}
}
pub fn as_alias(&self) -> Option<&Alias> {
match self {
Task::Alias(alias) => Some(alias),
_ => None,
}
}
pub fn is_executable(&self) -> bool {
match self {
Task::Plain(_) | Task::Custom(_) | Task::Execute(_) => true,
Task::Alias(_) => false,
}
}
pub fn as_command(&self) -> Option<CmdArgs> {
match self {
Task::Plain(str) => Some(CmdArgs::Single(str.clone())),
Task::Custom(custom) => Some(custom.cmd.clone()),
Task::Execute(exe) => Some(exe.cmd.clone()),
Task::Alias(_) => None,
}
}
pub fn as_single_command(&self) -> Option<Cow<str>> {
match self {
Task::Plain(str) => Some(Cow::Borrowed(str)),
Task::Custom(custom) => Some(custom.cmd.as_single()),
Task::Execute(exe) => Some(exe.cmd.as_single()),
Task::Alias(_) => None,
}
}
pub fn working_directory(&self) -> Option<&Path> {
match self {
Task::Plain(_) => None,
Task::Custom(custom) => custom.cwd.as_deref(),
Task::Execute(exe) => exe.cwd.as_deref(),
Task::Alias(_) => None,
}
}
pub fn is_custom(&self) -> bool {
matches!(self, Task::Custom(_))
}
}
#[serde_as]
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Execute {
pub cmd: CmdArgs,
#[serde(default)]
#[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")]
pub depends_on: Vec<TaskName>,
pub cwd: Option<PathBuf>,
}
impl From<Execute> for Task {
fn from(value: Execute) -> Self {
Task::Execute(value)
}
}
#[derive(Debug, Clone)]
pub struct Custom {
pub cmd: CmdArgs,
pub cwd: Option<PathBuf>,
}
impl From<Custom> for Task {
fn from(value: Custom) -> Self {
Task::Custom(value)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum CmdArgs {
Single(String),
Multiple(Vec<String>),
}
impl From<Vec<String>> for CmdArgs {
fn from(value: Vec<String>) -> Self {
CmdArgs::Multiple(value)
}
}
impl From<String> for CmdArgs {
fn from(value: String) -> Self {
CmdArgs::Single(value)
}
}
impl CmdArgs {
pub fn as_single(&self) -> Cow<str> {
match self {
CmdArgs::Single(cmd) => Cow::Borrowed(cmd),
CmdArgs::Multiple(args) => Cow::Owned(args.iter().map(|arg| quote(arg)).join(" ")),
}
}
pub fn into_single(self) -> String {
match self {
CmdArgs::Single(cmd) => cmd,
CmdArgs::Multiple(args) => args.iter().map(|arg| quote(arg)).join(" "),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde_as]
pub struct Alias {
#[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")]
pub depends_on: Vec<TaskName>,
}
impl Display for Task {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Task::Plain(cmd) => {
write!(f, "{}", cmd)?;
}
Task::Execute(cmd) => {
match &cmd.cmd {
CmdArgs::Single(cmd) => write!(f, "{}", cmd)?,
CmdArgs::Multiple(mult) => write!(f, "{}", mult.join(" "))?,
};
if !cmd.depends_on.is_empty() {
write!(f, ", ")?;
}
}
_ => {}
};
let depends_on = self.depends_on();
if !depends_on.is_empty() {
if depends_on.len() == 1 {
write!(
f,
"depends_on = '{}'",
depends_on.iter().map(|t| t.fancy_display()).join(",")
)
} else {
write!(
f,
"depends_on = [{}]",
depends_on.iter().map(|t| t.fancy_display()).join(",")
)
}
} else {
Ok(())
}
}
}
pub fn quote(in_str: &str) -> Cow<str> {
if in_str.is_empty() {
"\"\"".into()
} else if in_str
.bytes()
.any(|c| matches!(c as char, '\t' | '\r' | '\n' | ' ' | '[' | ']'))
{
let mut out: Vec<u8> = Vec::new();
out.push(b'"');
for c in in_str.bytes() {
match c as char {
'"' | '\\' => out.push(b'\\'),
_ => (),
}
out.push(c);
}
out.push(b'"');
unsafe { String::from_utf8_unchecked(out) }.into()
} else {
in_str.into()
}
}
pub fn quote_arguments<'a>(args: impl IntoIterator<Item = &'a str>) -> String {
args.into_iter().map(quote).join(" ")
}
#[cfg(test)]
mod tests {
use super::quote;
#[test]
fn test_quote() {
assert_eq!(quote("foobar"), "foobar");
assert_eq!(quote("foo bar"), "\"foo bar\"");
assert_eq!(quote("\""), "\"");
assert_eq!(quote("foo \" bar"), "\"foo \\\" bar\"");
assert_eq!(quote(""), "\"\"");
assert_eq!(quote("$PATH"), "$PATH");
assert_eq!(
quote("PATH=\"$PATH;build/Debug\""),
"PATH=\"$PATH;build/Debug\""
);
assert_eq!(quote("name=[64,64]"), "\"name=[64,64]\"");
}
}