use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use clap::ValueHint;
use color_eyre::eyre::{eyre, Result};
use duct::IntoExecutablePath;
use crate::cli::args::runtime::{RuntimeArg, RuntimeArgParser};
use crate::cli::command::Command;
#[cfg(test)]
use crate::cmd;
use crate::config::Config;
use crate::config::MissingRuntimeBehavior::Ignore;
use crate::env;
use crate::output::Output;
use crate::toolset::ToolsetBuilder;
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "x", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Exec {
#[clap(value_parser = RuntimeArgParser)]
pub runtime: Vec<RuntimeArg>,
#[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(visible_short_alias = 'C', value_hint = ValueHint::DirPath, long)]
pub cd: Option<PathBuf>,
}
impl Command for Exec {
fn run(self, mut config: Config, _out: &mut Output) -> Result<()> {
let ts = ToolsetBuilder::new()
.with_args(&self.runtime)
.with_install_missing()
.build(&mut config)?;
let (program, args) = parse_command(&env::SHELL, &self.command, &self.c);
let mut env = ts.env_with_path(&config);
if config.settings.missing_runtime_behavior != Ignore {
env.insert("RTX_MISSING_RUNTIME_BEHAVIOR".into(), "warn".into());
}
self.exec(program, args, env)
}
}
impl Exec {
#[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();
if let Some(cd) = &self.cd {
env::set_current_dir(cd)?;
}
let err = exec::Command::new(program.clone()).args(&args).exec();
Err(eyre!("{:?} {}", program.to_string_lossy(), err.to_string()))
}
#[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);
if let Some(cd) = &self.cd {
cmd = cmd.dir(cd);
}
for (k, v) in env.iter() {
cmd = cmd.env(k, v);
}
let res = cmd.unchecked().run()?;
match res.status.code().unwrap_or(1) {
0 => Ok(()),
code => Err(eyre!("command failed with exit code {}", code)),
}
}
}
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>rtx exec nodejs@18 -- node ./app.js</bold> # launch app.js using node-18.x
$ <bold>rtx x nodejs@18 -- node ./app.js</bold> # shorter alias
# Specify command as a string:
$ <bold>rtx exec nodejs@18 python@3.11 --command "node -v && python -V"</bold>
# Run a command in a different directory:
$ <bold>rtx x -C /path/to/project nodejs@18 -- node ./app.js</bold>
"#
);
#[cfg(test)]
mod tests {
use crate::assert_cli;
use crate::cli::tests::cli_run;
#[test]
fn test_exec_ok() {
assert_cli!("exec", "--", "echo");
}
#[test]
fn test_exec_fail() {
let _ = cli_run(
&vec!["rtx", "exec", "--", "exit", "1"]
.into_iter()
.map(String::from)
.collect::<Vec<String>>(),
)
.unwrap_err();
}
#[test]
fn test_exec_cd() {
assert_cli!("exec", "-C", "/tmp", "--", "pwd");
}
}