use std::collections::BTreeMap;
use std::ffi::OsString;
use clap::ValueHint;
use duct::IntoExecutablePath;
#[cfg(not(any(test, windows)))]
use eyre::{bail, Result};
#[cfg(any(test, windows))]
use eyre::{eyre, Result};
use crate::cli::args::ToolArg;
#[cfg(any(test, windows))]
use crate::cmd;
use crate::config::{Config, SETTINGS};
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<String>>,
#[clap(short, long = "command", value_hint = ValueHint::CommandString, conflicts_with = "command")]
pub c: Option<String>,
#[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::get();
let mut ts = measure!("toolset", {
ToolsetBuilder::new()
.with_args(&self.tool)
.with_default_to_latest(true)
.build(&config)?
});
let opts = InstallOptions {
force: false,
jobs: self.jobs,
raw: self.raw,
missing_args_only: !self.tool.is_empty()
|| !SETTINGS.exec_auto_install
|| !console::user_attended_stderr()
|| *env::__MISE_SHIM,
resolve_options: Default::default(),
..Default::default()
};
measure!("install_arg_versions", {
ts.install_missing_versions(&opts)?
});
measure!("notify_if_versions_missing", {
ts.notify_if_versions_missing()
});
let (program, mut args) = parse_command(&env::SHELL, &self.command, &self.c);
let env = measure!("env_with_path", { ts.env_with_path(&config)? });
if program.rsplit('/').next() == Some("fish") {
let mut cmd = vec![];
for (k, v) in env.iter().filter(|(k, _)| *k != "PATH") {
cmd.push(format!(
"set -gx {} {}",
shell_escape::escape(k.into()),
shell_escape::escape(v.into())
));
}
let (_, env_results) = ts.final_env(&config)?;
for p in ts.list_final_paths(&config, env_results)? {
cmd.push(format!(
"fish_add_path -gm {}",
shell_escape::escape(p.to_string_lossy())
));
}
args.insert(0, cmd.join("\n"));
args.insert(0, "-C".into());
}
time!("exec");
self.exec(program, args, env)
}
#[cfg(all(not(test), unix))]
fn exec<T, U>(&self, program: T, args: U, env: BTreeMap<String, String>) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
{
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(all(windows, not(test)))]
fn exec<T, U>(&self, program: T, args: U, env: BTreeMap<String, String>) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
{
let cwd = crate::dirs::CWD.clone().unwrap_or_default();
let program = program.to_executable();
let path = env.get(&*env::PATH_KEY).map(OsString::from);
let program = which::which_in(program, path, cwd)?;
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")),
}
}
#[cfg(test)]
fn exec<T, U>(&self, program: T, args: U, env: BTreeMap<String, String>) -> Result<()>
where
T: IntoExecutablePath,
U: IntoIterator,
U::Item: Into<OsString>,
{
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<String>>,
c: &Option<String>,
) -> (String, Vec<String>) {
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>
"#
);