use std::{ collections::HashMap, env::{self}, path::PathBuf, }; use directories::UserDirs; use mlua::prelude::*; use tokio::process::Command; mod kind; mod stdio; pub(super) use kind::*; pub(super) use stdio::*; #[derive(Debug, Clone, Default)] pub(super) struct ProcessSpawnOptions { pub cwd: Option, pub envs: HashMap, pub shell: Option, pub stdio: ProcessSpawnOptionsStdio, } impl<'lua> FromLua<'lua> for ProcessSpawnOptions { fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { let mut this = Self::default(); let value = match value { LuaValue::Nil => return Ok(this), LuaValue::Table(t) => t, _ => { return Err(LuaError::FromLuaConversionError { from: value.type_name(), to: "ProcessSpawnOptions", message: Some(format!( "Invalid spawn options - expected table, got {}", value.type_name() )), }) } }; /* If we got a working directory to use: 1. Substitute leading tilde (~) for the users home dir 2. Make sure it exists */ match value.get("cwd")? { LuaValue::Nil => {} LuaValue::String(s) => { let mut cwd = PathBuf::from(s.to_str()?); if let Ok(stripped) = cwd.strip_prefix("~") { let user_dirs = UserDirs::new().ok_or_else(|| { LuaError::runtime( "Invalid value for option 'cwd' - failed to get home directory", ) })?; cwd = user_dirs.home_dir().join(stripped); } if !cwd.exists() { return Err(LuaError::runtime( "Invalid value for option 'cwd' - path does not exist", )); }; this.cwd = Some(cwd); } value => { return Err(LuaError::RuntimeError(format!( "Invalid type for option 'cwd' - expected string, got '{}'", value.type_name() ))) } } /* If we got environment variables, make sure they are strings */ match value.get("env")? { LuaValue::Nil => {} LuaValue::Table(e) => { for pair in e.pairs::() { let (k, v) = pair.context("Environment variables must be strings")?; this.envs.insert(k, v); } } value => { return Err(LuaError::RuntimeError(format!( "Invalid type for option 'env' - expected table, got '{}'", value.type_name() ))) } } /* If we got a shell to use: 1. When given as a string, use that literally 2. When set to true, use a default shell for the platform */ match value.get("shell")? { LuaValue::Nil => {} LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()), LuaValue::Boolean(true) => { this.shell = match env::consts::FAMILY { "unix" => Some("/bin/sh".to_string()), "windows" => Some("powershell".to_string()), _ => None, }; } value => { return Err(LuaError::RuntimeError(format!( "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'", value.type_name() ))) } } /* If we got options for stdio handling, parse those as well - note that we accept a separate "stdin" value here for compatibility with older scripts, but the user should preferrably pass it in the stdio table */ this.stdio = value.get("stdio")?; match value.get("stdin")? { LuaValue::Nil => {} LuaValue::String(s) => this.stdio.stdin = Some(s.as_bytes().to_vec()), value => { return Err(LuaError::RuntimeError(format!( "Invalid type for option 'stdin' - expected 'string', got '{}'", value.type_name() ))) } } Ok(this) } } impl ProcessSpawnOptions { pub fn into_command(self, program: impl Into, args: Option>) -> Command { let mut program = program.into(); // Run a shell using the command param if wanted let pargs = match self.shell { None => args, Some(shell) => { let shell_args = match args { Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))], None => vec!["-c".to_string(), program.to_string()], }; program = shell.to_string(); Some(shell_args) } }; // Create command with the wanted options let mut cmd = match pargs { None => Command::new(program), Some(args) => { let mut cmd = Command::new(program); cmd.args(args); cmd } }; // Set dir to run in and env variables if let Some(cwd) = self.cwd { cmd.current_dir(cwd); } if !self.envs.is_empty() { cmd.envs(self.envs); } cmd } }