use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use clap::ValueHint;
use duct::IntoExecutablePath;
use eyre::Result;
use crate::cli::args::ToolArg;
#[cfg(test)]
use crate::cmd;
use crate::config::Config;
use crate::env;
use crate::toolset::{InstallOptions, ToolsetBuilder};
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "x", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Exec {
#[clap(value_name = "TOOL@VERSION")]
pub tool: Vec<ToolArg>,
#[clap(conflicts_with = "c", required_unless_present = "c", last = true)]
pub command: Option<Vec<OsString>>,
#[clap(short, long = "command", value_hint = ValueHint::CommandString, conflicts_with = "command")]
pub c: Option<OsString>,
#[clap(long, short, env = "MISE_JOBS", verbatim_doc_comment)]
pub jobs: Option<usize>,
#[clap(long, overrides_with = "jobs")]
pub raw: bool,
}
impl Exec {
pub fn run(self) -> Result<()> {
let config = Config::try_get()?;
let mut ts = ToolsetBuilder::new().with_args(&self.tool).build(&config)?;
let opts = InstallOptions {
force: false,
jobs: self.jobs,
raw: self.raw,
latest_versions: false,
};
ts.install_arg_versions(&config, &opts)?;
ts.notify_if_versions_missing();
let (program, args) = parse_command(&env::SHELL, &self.command, &self.c);
let env = ts.env_with_path(&config);
self.exec(program, args, env)
}
#[cfg(not(test))]
fn exec<T, U, E>(&self, program: T, args: U, env: BTreeMap<E, E>) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
E: AsRef<OsStr>,
{
for (k, v) in env.iter() {
env::set_var(k, v);
}
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
let program = program.to_executable();
let err = exec::Command::new(program.clone()).args(&args).exec();
bail!("{:?} {err}", program.to_string_lossy())
}
#[cfg(test)]
fn exec<T, U, E>(&self, program: T, args: U, env: BTreeMap<E, E>) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
E: AsRef<OsStr>,
{
let mut cmd = cmd::cmd(program, args);
for (k, v) in env.iter() {
cmd = cmd.env(k, v);
}
let res = cmd.unchecked().run()?;
match res.status.code() {
Some(0) => Ok(()),
Some(code) => Err(eyre!("command failed: exit code {}", code)),
None => Err(eyre!("command failed: terminated by signal")),
}
}
}
fn parse_command(
shell: &str,
command: &Option<Vec<OsString>>,
c: &Option<OsString>,
) -> (OsString, Vec<OsString>) {
match (&command, &c) {
(Some(command), _) => {
let (program, args) = command.split_first().unwrap();
(program.clone(), args.into())
}
_ => (shell.into(), vec!["-c".into(), c.clone().unwrap()]),
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise exec node@20 -- node ./app.js</bold> # launch app.js using node-20.x
$ <bold>mise x node@20 -- node ./app.js</bold> # shorter alias
# Specify command as a string:
$ <bold>mise exec node@20 python@3.11 --command "node -v && python -V"</bold>
# Run a command in a different directory:
$ <bold>mise x -C /path/to/project node@20 -- node ./app.js</bold>
"#
);
#[cfg(test)]
mod tests {
use std::env;
#[test]
fn test_exec_ok() {
assert_cli!("exec", "--", "echo");
}
#[test]
fn test_exec_fail() {
let err = assert_cli_err!("exec", "--", "exit", "1");
assert_display_snapshot!(err);
}
#[test]
fn test_exec_cd() {
let cwd = env::current_dir().unwrap();
assert_cli!("exec", "-C", "/tmp", "--", "pwd");
env::set_current_dir(cwd).unwrap();
}
}