use crate::cli::atomic::should_dry_run;
use crate::config::LoadedConfig;
use crate::util::logging::{BOLD, RESET};
use crate::{log_dry, log_exec, log_warn};
use anyhow::{Context, Result, anyhow, bail};
use regex::Regex;
use std::collections::HashMap;
use std::env;
use tokio::process::Command;
use tokio::task;
struct ExecJob {
pub name: String,
pub run: String,
pub sudo: bool,
pub ensure_first: bool,
pub flag: bool,
pub required: Vec<String>,
}
fn extract_cmd(config: &LoadedConfig, name: &str) -> Result<ExecJob> {
let command_map = config
.command
.as_ref()
.ok_or_else(|| anyhow!("no command exists"))?;
let command = command_map
.get(name)
.cloned()
.ok_or_else(|| anyhow!("no such command {name}"))?;
let run = substitute(&command.run, config.vars.clone())?;
let sudo = command.sudo.unwrap_or_default();
let flag = command.flag.unwrap_or_default();
let ensure_first = command.ensure_first.unwrap_or_default();
let required = command.required.unwrap_or_default();
Ok(ExecJob {
name: name.to_string(),
run,
sudo,
ensure_first,
flag,
required,
})
}
#[must_use]
fn extract_all_cmds(config: &LoadedConfig) -> Vec<ExecJob> {
let mut jobs = Vec::new();
if let Some(command_map) = config.command.as_ref() {
for name in command_map.keys() {
if let Ok(job) = extract_cmd(config, name) {
jobs.push(job);
}
}
}
jobs
}
fn substitute(text: &str, vars: Option<HashMap<String, String>>) -> Result<String> {
let re = Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)|\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
.with_context(
|| "Failed to construct regex pattern for external cmd variable substitution.",
)?;
let resolve_var = |var_name: &str| {
vars.as_ref()
.and_then(|map| map.get(var_name))
.cloned()
.or_else(|| env::var(var_name).ok())
.unwrap_or_else(|| format!("${{{var_name}}}"))
};
let result = re.replace_all(text, |caps: ®ex::Captures| {
let var_name = caps
.get(1)
.or_else(|| caps.get(2))
.map_or("", |m| m.as_str());
resolve_var(var_name)
});
Ok(result.into_owned())
}
async fn execute_command(job: ExecJob, dry_run: bool) -> Result<()> {
let (bin, args) = if job.sudo {
("sudo", vec!["sh", "-c", &job.run])
} else {
("sh", vec!["-c", &job.run])
};
if dry_run {
log_dry!("Would execute: {bin} {}", job.run);
return Ok(());
}
log_exec!("{BOLD}{}{RESET}", job.name);
let mut child = Command::new(bin).args(&args).spawn()?;
let status = child.wait().await?;
if !status.success() {
bail!(format!("Command {} failed to execute.", job.name))
}
Ok(())
}
fn all_bins_present(required: &[String]) -> bool {
let mut present = true;
if !required.is_empty() {
for bin in required {
if which::which(bin).is_err() {
log_warn!("{bin} not found in $PATH.");
present = false;
}
}
}
present
}
#[derive(PartialEq, Eq)]
pub enum ExecMode {
Regular,
All,
Flagged,
}
pub async fn run_all(config: LoadedConfig, mode: ExecMode) -> Result<i32> {
let cmds = extract_all_cmds(&config);
let mut ensure_first_cmds = Vec::new();
let mut regular_cmds = Vec::new();
for job in cmds {
if !all_bins_present(&job.required)
|| (mode == ExecMode::Regular && job.flag)
|| (mode == ExecMode::Flagged && !job.flag)
{
continue;
} else if job.ensure_first {
ensure_first_cmds.push(job);
} else {
regular_cmds.push(job);
}
}
let dry_run = should_dry_run();
let mut failures = 0;
let mut successes = 0;
for job in ensure_first_cmds {
if (execute_command(job, dry_run).await).is_err() {
failures += 1;
} else {
successes += 1;
}
}
let mut handles = Vec::new();
for job in regular_cmds {
handles.push(task::spawn(
async move { execute_command(job, dry_run).await },
));
}
for handle in handles {
if handle.await?.is_err() {
failures += 1;
} else {
successes += 1;
}
}
if failures > 0 {
log_warn!("{failures} external commands failed",);
} else if successes == 0 {
log_warn!("No regular external commands found. Maybe you meant flagged or all?",);
}
Ok(successes)
}
pub async fn run_one(config: LoadedConfig, name: &str) -> Result<()> {
let state = extract_cmd(&config, name)?;
if !all_bins_present(&state.required) {
bail!("Cannot execute command due to missing binaries.")
}
let dry_run = should_dry_run();
execute_command(state, dry_run).await
}