use {
anyhow::Context,
std::{fs::File, io::Write, path::Path, process},
};
use crate::command::{PredefinedShell, Shell, UserCommand};
fn resolve_working_dir(cmd: &UserCommand) -> Option<std::path::PathBuf> {
let working_dir = cmd.working_dir.as_ref()?;
let project_dir = cmd.project_dir.as_ref()?;
Some(project_dir.join(working_dir))
}
pub fn execute_command(cmd: &UserCommand, args: &[String]) -> anyhow::Result<process::ExitStatus> {
let default_shell = Shell::default();
let shell = cmd.shell.as_ref().unwrap_or(&default_shell);
let env_vars: Vec<(String, String)> = cmd
.args
.iter()
.zip(args.iter())
.map(|(def, val)| (def.name.clone(), val.clone()))
.collect();
execute_script(
&cmd.script,
args,
shell,
resolve_working_dir(cmd).as_deref(),
&env_vars,
)
}
pub fn execute_script(
script: &str,
args: &[String],
shell: &Shell,
work_dir: Option<&Path>,
env_vars: &[(String, String)],
) -> anyhow::Result<process::ExitStatus> {
match shell {
Shell::Predefined(p) => match p {
PredefinedShell::Sh => {
execute_script_unix(script, args, "script", "/bin/sh", work_dir, env_vars)
}
PredefinedShell::Bash => {
execute_script_unix(script, args, "script", "/bin/bash", work_dir, env_vars)
}
PredefinedShell::Zsh => {
execute_script_unix(script, args, "script", "/bin/zsh", work_dir, env_vars)
}
PredefinedShell::Fish => execute_script_unix(
script,
args,
"script",
"/usr/bin/env fish",
work_dir,
env_vars,
),
PredefinedShell::Cmd => execute_script_cmd(script, args, work_dir, env_vars),
PredefinedShell::Powershell => {
execute_script_powershell(script, args, work_dir, env_vars)
}
},
Shell::Custom {
command,
extension,
shebang,
} => {
if let Some(shebang) = shebang {
execute_script_unix(
script,
args,
&format!("script{extension}"),
shebang,
work_dir,
env_vars,
)
} else {
execute_script_custom_command(script, args, command, extension, work_dir, env_vars)
}
}
}
}
fn configure_cmd(
cmd: &mut process::Command,
work_dir: Option<&Path>,
env_vars: &[(String, String)],
) {
if let Some(dir) = work_dir {
cmd.current_dir(dir);
}
for (key, value) in env_vars {
cmd.env(key, value);
}
}
fn execute_script_unix(
script: &str,
args: &[String],
filename: &str,
shebang: &str,
work_dir: Option<&Path>,
env_vars: &[(String, String)],
) -> anyhow::Result<process::ExitStatus> {
let tmp_dir = tempfile::tempdir()?;
let file_path = tmp_dir.path().join(filename);
{
let mut tmp_file = File::create(&file_path)?;
writeln!(tmp_file, "#!{shebang}")?;
tmp_file.write_all(script.as_bytes())?;
tmp_file.flush()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = tmp_file.metadata()?.permissions();
permissions.set_mode(0o500);
tmp_file.set_permissions(permissions)?;
}
}
let mut cmd = process::Command::new(file_path);
cmd.args(args);
configure_cmd(&mut cmd, work_dir, env_vars);
let mut child = cmd.spawn().context("script command failed to start")?;
let status = child.wait()?;
tmp_dir.close()?;
Ok(status)
}
fn execute_script_custom_command(
script: &str,
args: &[String],
command: &str,
extension: &str,
work_dir: Option<&Path>,
env_vars: &[(String, String)],
) -> anyhow::Result<process::ExitStatus> {
let tmp_dir = tempfile::tempdir()?;
let file_path = tmp_dir.path().join(format!("script{extension}"));
{
let mut tmp_file = File::create(&file_path)?;
tmp_file.write_all(script.as_bytes())?;
tmp_file.flush()?;
}
let mut cmd = process::Command::new(command);
cmd.arg(&file_path).args(args);
configure_cmd(&mut cmd, work_dir, env_vars);
let mut child = cmd
.spawn()
.context("custom shell command failed to start")?;
let status = child.wait()?;
tmp_dir.close()?;
Ok(status)
}
fn execute_script_cmd(
script: &str,
args: &[String],
work_dir: Option<&Path>,
env_vars: &[(String, String)],
) -> anyhow::Result<process::ExitStatus> {
let tmp_dir = tempfile::tempdir()?;
let file_path = tmp_dir.path().join("script.bat");
{
let mut tmp_file = File::create(&file_path)?;
tmp_file.write_all(b"@echo off\n")?;
tmp_file.write_all(script.as_bytes())?;
tmp_file.flush()?;
}
let mut cmd = process::Command::new(file_path);
cmd.args(args);
configure_cmd(&mut cmd, work_dir, env_vars);
let mut child = cmd.spawn().context("script command failed to start")?;
let status = child.wait()?;
tmp_dir.close()?;
Ok(status)
}
fn execute_script_powershell(
script: &str,
args: &[String],
work_dir: Option<&Path>,
env_vars: &[(String, String)],
) -> anyhow::Result<process::ExitStatus> {
let tmp_dir = tempfile::tempdir()?;
let file_path = tmp_dir.path().join("script.ps1");
{
let mut tmp_file = File::create(&file_path)?;
tmp_file.write_all(script.as_bytes())?;
tmp_file.flush()?;
}
let powershell_cmd: Vec<_> = ["&".to_string(), format!("{}", file_path.display())]
.into_iter()
.chain(args.iter().map(|arg| snailquote::escape(arg).into_owned()))
.collect();
let powershell_cmd = powershell_cmd.join(" ");
let args = [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
&powershell_cmd,
];
let mut cmd = process::Command::new("PowerShell");
cmd.args(args);
configure_cmd(&mut cmd, work_dir, env_vars);
let mut child = cmd.spawn().context("PowerShell command failed to start")?;
let status = child.wait()?;
tmp_dir.close()?;
Ok(status)
}
#[cfg(test)]
mod tests {
use {super::*, std::path::PathBuf};
fn base_cmd() -> UserCommand {
UserCommand {
id: None,
name: "test".into(),
description: None,
script: "echo hi".into(),
args: vec![],
tags: vec![],
only_on: None,
shell: None,
only_in_dir: vec![],
risky: false,
after_run: None,
working_dir: None,
source_path: None,
project_dir: None,
domain: None,
}
}
#[test]
fn working_dir_relative_to_project_root() {
let cmd = UserCommand {
working_dir: Some("src".into()),
project_dir: Some(PathBuf::from("/project")),
..base_cmd()
};
assert_eq!(
resolve_working_dir(&cmd),
Some(PathBuf::from("/project/src"))
);
}
#[test]
fn working_dir_dot_resolves_to_project_root() {
let cmd = UserCommand {
working_dir: Some(".".into()),
project_dir: Some(PathBuf::from("/project")),
..base_cmd()
};
assert_eq!(resolve_working_dir(&cmd), Some(PathBuf::from("/project/.")));
}
#[test]
fn working_dir_nested_subdir() {
let cmd = UserCommand {
working_dir: Some("src/lib".into()),
project_dir: Some(PathBuf::from("/project")),
..base_cmd()
};
assert_eq!(
resolve_working_dir(&cmd),
Some(PathBuf::from("/project/src/lib"))
);
}
#[test]
fn working_dir_none_without_field() {
let cmd = UserCommand {
project_dir: Some(PathBuf::from("/project")),
..base_cmd()
};
assert!(resolve_working_dir(&cmd).is_none());
}
#[test]
fn working_dir_none_without_project_dir() {
let cmd = UserCommand {
working_dir: Some("subdir".into()),
..base_cmd()
};
assert!(resolve_working_dir(&cmd).is_none());
}
#[test]
fn working_dir_none_both_unset() {
assert!(resolve_working_dir(&base_cmd()).is_none());
}
}