use super::perform_assignments;
use crate::Handle as _;
use crate::Runtime;
use crate::redir::RedirGuard;
use crate::xtrace::XTrace;
use crate::xtrace::print;
use itertools::Itertools as _;
use std::ops::ControlFlow::{Break, Continue};
use std::rc::Rc;
use yash_env::Env;
use yash_env::io::print_error;
use yash_env::job::handle_job_status;
use yash_env::semantics::Divert;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Result;
use yash_env::subshell::Config;
use yash_syntax::syntax::Assign;
use yash_syntax::syntax::Redir;
pub async fn execute_absent_target<S: Runtime + 'static>(
env: &mut Env<S>,
assigns: &[Assign],
redirs: &Rc<Vec<Redir>>,
exit_status: ExitStatus,
) -> Result {
let redir_exit_status = if let Some(redir) = redirs.first() {
let first_redir_location = redir.body.operand().location.clone();
let redirs_2 = Rc::clone(redirs);
let subshell = Config::foreground().start_and_wait(env, async move |env, _job_control| {
let env = &mut RedirGuard::new(env);
let mut xtrace = XTrace::from_options(&env.options);
let redir_exit_status = match env.perform_redirs(redirs_2.iter(), xtrace.as_mut()).await
{
Ok(exit_status) => exit_status,
Err(error) => {
let result = error.handle(env).await;
env.apply_result(result);
return;
}
};
print(env, xtrace).await;
env.exit_status = redir_exit_status.unwrap_or(exit_status);
});
match subshell.await {
Ok((pid, result)) => handle_job_status(env, pid, result, || {
redirs
.iter()
.format_with(" ", |redir, f| f(&format_args!("{redir}")))
.to_string()
})?,
Err(errno) => {
print_error(
env,
"cannot start subshell to perform redirection".into(),
errno.to_string().into(),
&first_redir_location,
)
.await;
return Break(Divert::Interrupt(Some(ExitStatus::ERROR)));
}
}
} else {
exit_status
};
let mut xtrace = XTrace::from_options(&env.options);
let assignment_exit_status = perform_assignments(env, assigns, false, xtrace.as_mut()).await?;
print(env, xtrace).await;
env.exit_status = assignment_exit_status.unwrap_or(redir_exit_status);
Continue(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::Command as _;
use crate::tests::return_builtin;
use futures_util::FutureExt as _;
use std::assert_matches;
use std::rc::Rc;
use yash_env::VirtualSystem;
use yash_env::option::State::On;
use yash_env::system::Concurrent;
use yash_env::system::r#virtual::FileBody;
use yash_env::test_helper::assert_stderr;
use yash_env::test_helper::in_virtual_system;
use yash_env::variable::Scope;
use yash_env::variable::Value;
use yash_syntax::source::Location;
use yash_syntax::syntax;
#[test]
fn simple_command_performs_redirection_with_absent_target() {
in_virtual_system(|mut env, state| async move {
let command: syntax::SimpleCommand = ">/tmp/foo".parse().unwrap();
let result = command.execute(&mut env).await;
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
let file = state.borrow().file_system.get("/tmp/foo").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } if content.is_empty());
});
}
#[test]
fn simple_command_returns_command_substitution_exit_status_from_redirection() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("return", return_builtin());
let command: syntax::SimpleCommand = ">/tmp/foo$(return -n 42)".parse().unwrap();
_ = command.execute(&mut env).await;
assert_eq!(env.exit_status, ExitStatus(42));
});
}
#[test]
fn simple_command_handles_redirection_error_with_absent_target() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("return", return_builtin());
let command = &"$(return -n 11) < /no/such/file$(return -n 22)";
let command: syntax::SimpleCommand = command.parse().unwrap();
_ = command.execute(&mut env).await;
assert_eq!(env.exit_status, ExitStatus::ERROR);
});
}
#[test]
fn simple_command_handles_subshell_error_with_absent_target() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
let command: syntax::SimpleCommand = ">/tmp/foo".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn simple_command_performs_assignment_with_absent_target() {
let mut env = Env::new_virtual();
let command: syntax::SimpleCommand = "a=b".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
assert_eq!(
env.variables.get("a").unwrap().value,
Some(Value::scalar("b"))
);
}
#[test]
fn simple_command_returns_command_substitution_exit_status_from_assignment() {
in_virtual_system(|mut env, _state| async move {
env.builtins.insert("return", return_builtin());
let command: syntax::SimpleCommand = "a=$(return -n 12)".parse().unwrap();
_ = command.execute(&mut env).await;
assert_eq!(env.exit_status, ExitStatus(12));
})
}
#[test]
fn simple_command_handles_assignment_error_with_absent_target() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
let mut var = env.variables.get_or_new("a", Scope::Global);
var.assign("", None).unwrap();
var.make_read_only(Location::dummy("ROL"));
let command: syntax::SimpleCommand = "a=b".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn xtrace_for_absent_target() {
in_virtual_system(|mut env, state| async move {
env.options.set(yash_env::option::XTrace, On);
let command: syntax::SimpleCommand = "FOO=bar 3>/dev/null".parse().unwrap();
let _ = command.execute(&mut env).await;
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "3>/dev/null\nFOO=bar\n");
});
});
}
}