use crate::Result;
use crate::runtime::Runtime;
use crate::script::lua_script::helpers::to_vec_of_strings;
use mlua::{Lua, Table, Value};
use std::process::Command;
pub fn init_module(lua: &Lua, _runtime: &Runtime) -> Result<Table> {
let table = lua.create_table()?;
let exec_fn = lua.create_function(cmd_exec)?;
table.set("exec", exec_fn)?;
Ok(table)
}
fn cmd_exec(lua: &Lua, (cmd_name, args): (String, Option<Value>)) -> mlua::Result<Value> {
let args = args.map(|args| to_vec_of_strings(args, "command args")).transpose()?;
let mut command = cross_command(&cmd_name, args)?;
match command.output() {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1) as i64;
let res = lua.create_table()?;
res.set("stdout", stdout.as_str())?;
res.set("stderr", stderr.as_str())?;
res.set("exit", exit_code)?;
if exit_code == 0 {
Ok(Value::Table(res))
} else {
res.set("error", format!("Command exited with non-zero status: {}", exit_code))?;
let cmd = command.get_program().to_str().unwrap_or_default();
let args = command
.get_args()
.map(|a| a.to_str().unwrap_or_default())
.collect::<Vec<&str>>();
let args = args.join(" ");
Err(crate::Error::custom(format!(
"\
Fail to execute: {cmd} {args}
stdout:\n{stdout}\n
stderr:\n{stderr}\n
exit code: {exit_code}\n"
))
.into())
}
}
Err(err) => {
let cmd = command.get_program().to_str().unwrap_or_default();
let args = command
.get_args()
.map(|a| a.to_str().unwrap_or_default())
.collect::<Vec<&str>>();
let args = args.join(" ");
Err(crate::Error::custom(format!(
"\
Fail to execute: {cmd} {args}
Cause:\n{err}"
))
.into())
}
}
}
fn cross_command(cmd_name: &str, args: Option<Vec<String>>) -> Result<Command> {
let command = if cfg!(windows) {
let full_cmd = if let Some(args) = args {
let joined = args.join(" ");
format!("{cmd_name} {joined}")
} else {
cmd_name.to_string()
};
let mut cmd = Command::new("cmd");
cmd.args(["/C", &full_cmd]);
cmd
} else {
let mut cmd = Command::new(cmd_name);
if let Some(args) = args {
cmd.args(args);
}
cmd
};
Ok(command)
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::{assert_contains, eval_lua, setup_lua};
use crate::script::lua_script::aip_cmd;
use value_ext::JsonValueExt as _;
#[tokio::test]
async fn test_lua_cmd_exec_echo_single_arg() -> Result<()> {
let lua = setup_lua(aip_cmd::init_module, "cmd")?;
let script = r#"
return aip.cmd.exec("echo", "hello world")
"#;
let res = eval_lua(&lua, script)?;
assert_eq!(res.x_get_str("stdout")?.trim(), "hello world");
assert_eq!(res.x_get_str("stderr")?, "");
assert_eq!(res.x_get_i64("exit")?, 0);
Ok(())
}
#[tokio::test]
async fn test_lua_cmd_exec_echo_multiple_args() -> Result<()> {
let lua = setup_lua(aip_cmd::init_module, "cmd")?;
let script = r#"
return aip.cmd.exec("echo", {"hello", "world"})
"#;
let res = eval_lua(&lua, script)?;
assert_eq!(res.x_get_str("stdout")?.trim(), "hello world");
assert_eq!(res.x_get_str("stderr")?, "");
assert_eq!(res.x_get_i64("exit")?, 0);
Ok(())
}
#[tokio::test]
async fn test_lua_cmd_exec_invalid_command_pcall() -> Result<()> {
let lua = setup_lua(aip_cmd::init_module, "cmd")?;
let script = r#"
local ok, err = pcall(function()
return aip.cmd.exec("nonexistentcommand")
end)
return err -- to trigger the error on the rust side
"#;
let Err(err) = eval_lua(&lua, script) else {
return Err("Should have returned an error".into());
};
let err = err.to_string();
assert_contains(&err, "nonexistentcommand");
Ok(())
}
#[tokio::test]
async fn test_lua_cmd_exec_invalid_command_direct() -> Result<()> {
let lua = setup_lua(aip_cmd::init_module, "cmd")?;
let script = r#"return aip.cmd.exec("nonexistentcommand")"#;
let Err(err) = eval_lua(&lua, script) else {
return Err("Should have returned an error".into());
};
let err = err.to_string();
assert_contains(&err, "nonexistentcommand");
Ok(())
}
}