use super::perform_assignments;
use crate::Handle as _;
use crate::Runtime;
use crate::command::search::search_path;
use crate::redir::RedirGuard;
use crate::xtrace::XTrace;
use crate::xtrace::print;
use crate::xtrace::trace_fields;
use either::Either;
use futures_util::future::{Either as SelectResult, select};
use std::ops::ControlFlow::{Break, Continue};
use std::pin::pin;
use yash_env::Env;
use yash_env::builtin::Builtin;
use yash_env::io::print_error;
use yash_env::semantics::Divert;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::semantics::Result;
use yash_env::stack::Builtin as FrameBuiltin;
use yash_env::variable::Context;
use yash_syntax::syntax::Assign;
use yash_syntax::syntax::Redir;
pub async fn execute_builtin<S: Runtime + 'static>(
env: &mut Env<S>,
builtin: Builtin<S>,
assigns: &[Assign],
mut fields: Vec<Field>,
redirs: &[Redir],
) -> Result {
use yash_env::builtin::Type::*;
let mut xtrace = XTrace::from_options(&env.options);
trace_fields(xtrace.as_mut(), &fields);
let env = &mut RedirGuard::new(env);
if let Err(e) = env.perform_redirs(redirs, xtrace.as_mut()).await {
e.handle(env).await?;
return match builtin.r#type {
Special => Break(Divert::Interrupt(None)),
Mandatory | Elective | Extension | Substitutive => Continue(()),
};
};
let result = 'result: {
let is_special = builtin.r#type == Special;
let (mut env, export) = if is_special {
(Either::Left(&mut *env), false)
} else {
(Either::Right(env.push_context(Context::Volatile)), true)
};
let env = match &mut env {
Either::Left(e) => &mut ***e,
Either::Right(e) => &mut **e,
};
perform_assignments(env, assigns, export, xtrace.as_mut()).await?;
print(env, xtrace).await;
let name = fields.remove(0);
if builtin.r#type == Substitutive && search_path(env, &name.value).is_none() {
print_error(
env,
format!("cannot execute built-in utility {:?}", name.value).into(),
"utility not found in $PATH, so the built-in is ignored".into(),
&name.origin,
)
.await;
break 'result ExitStatus::NOT_FOUND.into();
}
let env = &mut env.push_frame(FrameBuiltin { name, is_special }.into());
let interruptible = !builtin.handles_signals_internally
&& env.is_interactive()
&& env.sigint_has_default_action();
if !interruptible {
break 'result (builtin.execute)(env, fields).await;
}
let mut caught = Vec::new();
let system = env.system.clone();
let result = {
let builtin_fut = (builtin.execute)(env, fields);
let sigint_fut = pin!(async {
loop {
let signals = system.wait_for_signals().await;
caught.extend(signals.iter().copied());
if signals.contains(&S::SIGINT) {
return;
}
}
});
match select(builtin_fut, sigint_fut).await {
SelectResult::Left((result, _sigint_fut)) => Some(result),
SelectResult::Right(((), _builtin_fut)) => None,
}
};
for signal in caught {
env.traps.catch_signal(signal);
}
match result {
Some(result) => result,
None => return Break(Divert::Interrupt(Some(ExitStatus::from(S::SIGINT)))),
}
};
if result.should_retain_redirs() {
env.preserve_redirs();
}
env.exit_status = result.exit_status();
result.divert()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::Command as _;
use crate::tests::echo_builtin;
use crate::tests::local_builtin;
use crate::tests::return_builtin;
use assert_matches::assert_matches;
use futures_util::FutureExt as _;
use futures_util::poll;
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use std::str::from_utf8;
use yash_env::VirtualSystem;
use yash_env::builtin::Type::{Elective, Extension, Mandatory, Special, Substitutive};
use yash_env::option::Interactive;
use yash_env::option::State::On;
use yash_env::semantics::ExitStatus;
use yash_env::stack::Frame;
use yash_env::system::Concurrent;
use yash_env::system::Errno;
use yash_env::system::Mode;
use yash_env::system::SendSignal as _;
use yash_env::system::Signals as _;
use yash_env::system::r#virtual::FileBody;
use yash_env::system::r#virtual::Inode;
use yash_env::system::r#virtual::SIGINT;
use yash_env::test_helper::assert_stderr;
use yash_env::test_helper::assert_stdout;
use yash_env::test_helper::in_virtual_system;
use yash_env::variable::Scope::Global;
use yash_env::variable::Value;
use yash_syntax::syntax;
#[test]
fn simple_command_returns_exit_status_from_builtin_without_divert() {
let mut env = Env::new_virtual();
env.builtins.insert("return", return_builtin());
let command: syntax::SimpleCommand = "return -n 93".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus(93));
}
#[test]
fn simple_command_returns_exit_status_from_builtin_with_divert() {
let mut env = Env::new_virtual();
env.builtins.insert(
"foo",
Builtin::new(Special, |_env, _args| {
Box::pin(std::future::ready({
yash_env::builtin::Result::with_exit_status_and_divert(
ExitStatus(37),
Break(Divert::Return(None)),
)
}))
}),
);
let command: syntax::SimpleCommand = "foo".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Return(None)));
assert_eq!(env.exit_status, ExitStatus(37));
}
#[test]
fn simple_command_applies_redirections_to_builtin() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("echo", echo_builtin());
let command: syntax::SimpleCommand = "echo hello >/tmp/file".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
let file = state.borrow().file_system.get("/tmp/file").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(from_utf8(content), Ok("hello\n"));
});
}
#[test]
fn simple_command_by_default_reverts_redirections_to_builtin() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("echo", echo_builtin());
let command: syntax::SimpleCommand = "echo hello >/tmp/file".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
let command: syntax::SimpleCommand = "echo world".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_stdout(&state, |stdout| assert_eq!(stdout, "world\n"));
}
#[test]
fn simple_command_retains_redirections_to_builtin_if_requested() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("echo", echo_builtin());
env.builtins.insert(
"exec",
Builtin::new(Mandatory, |_env, _args| {
Box::pin(async {
let mut result = yash_env::builtin::Result::default();
result.retain_redirs();
result
})
}),
);
let command: syntax::SimpleCommand = "exec >/tmp/file".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
let command: syntax::SimpleCommand = "echo hello".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
let file = state.borrow().file_system.get("/tmp/file").unwrap();
let file = file.borrow();
assert_matches!(&file.body, FileBody::Regular { content, .. } => {
assert_eq!(from_utf8(content), Ok("hello\n"));
});
}
#[test]
fn simple_command_skips_running_builtin_on_redirection_error() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("echo", echo_builtin());
let command: syntax::SimpleCommand = "echo X </no/such/file >/tmp/file".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::ERROR);
assert_eq!(
state.borrow().file_system.get("/tmp/file"),
Err(Errno::ENOENT)
);
assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn special_builtin_interrupts_on_redirection_error() {
let mut env = Env::new_virtual();
env.builtins.insert("return", return_builtin());
let command: syntax::SimpleCommand = "return </no/such/file".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Interrupt(None)));
assert_eq!(env.exit_status, ExitStatus::ERROR);
}
#[test]
fn simple_command_assigns_permanently_for_special_builtin() {
let mut env = Env::new_virtual();
env.builtins.insert("return", return_builtin());
let command: syntax::SimpleCommand = "v=42 return -n 0".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
let v = env.variables.get("v").unwrap();
assert_eq!(v.value, Some(Value::scalar("42")));
assert!(!v.is_exported);
}
#[test]
fn substitutive_builtin_must_be_found_in_path_after_assignments() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
let mut echo_builtin = echo_builtin();
echo_builtin.r#type = Substitutive;
env.builtins.insert("echo", echo_builtin);
let mut content = Inode::default();
content.body = FileBody::Regular {
content: Vec::new(),
is_native_executable: true,
};
content.permissions.set(Mode::USER_EXEC, true);
let content = Rc::new(RefCell::new(content));
state
.borrow_mut()
.file_system
.save("/usr/bin/echo", content)
.unwrap();
env.variables
.get_or_new("PATH", Global)
.assign("/bin:/usr/bin:/usr/local/bin", None)
.unwrap();
let command: syntax::SimpleCommand = "echo hello".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
let command: syntax::SimpleCommand = "PATH=/no/such/dir echo hello".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.exit_status, ExitStatus::NOT_FOUND);
assert_stderr(&state, |stderr| {
assert!(
stderr.contains("cannot execute built-in utility \"echo\""),
"stderr={stderr:?}"
);
});
}
#[test]
fn non_substitutive_builtins_must_be_run_regardless_of_path() {
for r#type in [Special, Mandatory, Elective, Extension] {
let mut env = Env::new_virtual();
let mut echo_builtin = echo_builtin();
echo_builtin.r#type = r#type;
env.builtins.insert("echo", echo_builtin);
let command: syntax::SimpleCommand = "echo hello".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
}
}
#[test]
fn simple_command_assigns_temporarily_for_regular_builtin() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("local", local_builtin());
let command: syntax::SimpleCommand = "v=42 local v".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.variables.get("v"), None);
assert_stdout(&state, |stdout| assert_eq!(stdout, "v=42\n"));
}
#[test]
fn simple_command_pushes_stack_frame_for_builtin() {
fn builtin_main(
env: &mut Env<Rc<Concurrent<VirtualSystem>>>,
_args: Vec<Field>,
) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
Box::pin(async {
assert_matches!(&env.stack[..], [Frame::Builtin(builtin)] => {
assert_eq!(builtin.name.value, "builtin");
assert!(!builtin.is_special);
});
Default::default()
})
}
fn special_main(
env: &mut Env<Rc<Concurrent<VirtualSystem>>>,
_args: Vec<Field>,
) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
Box::pin(async {
assert_matches!(&env.stack[..], [Frame::Builtin(builtin)] => {
assert_eq!(builtin.name.value, "special");
assert!(builtin.is_special);
});
Default::default()
})
}
let mut env = Env::new_virtual();
env.builtins
.insert("builtin", Builtin::new(Mandatory, builtin_main));
env.builtins
.insert("special", Builtin::new(Special, special_main));
let command: syntax::SimpleCommand = "builtin".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
let command: syntax::SimpleCommand = "special".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.stack[..], []);
}
#[test]
fn sigint_interrupts_builtin_by_default_if_interactive() {
in_virtual_system(|mut env, state| async move {
let system = VirtualSystem {
process_id: env.main_pid,
state,
};
env.options.set(Interactive, On);
env.traps
.enable_internal_dispositions_for_terminators(&env.system)
.await
.unwrap();
env.builtins.insert(
"foo",
Builtin::new(Mandatory, |_env, _args| Box::pin(std::future::pending())),
);
let command: syntax::SimpleCommand = "foo".parse().unwrap();
let mut execute_fut = pin!(command.execute(&mut env));
assert_eq!(poll!(execute_fut.as_mut()), std::task::Poll::Pending);
system.raise(VirtualSystem::SIGINT).await.unwrap();
let result = execute_fut.await;
assert_eq!(
result,
Break(Divert::Interrupt(Some(ExitStatus::from(SIGINT))))
);
})
}
#[test]
fn sigint_not_checked_when_handles_signals_internally_is_true() {
in_virtual_system(|mut env, state| async move {
let system = VirtualSystem {
process_id: env.main_pid,
state,
};
env.options.set(Interactive, On);
env.traps
.enable_internal_dispositions_for_terminators(&env.system)
.await
.unwrap();
let mut builtin =
Builtin::new(Mandatory, |_env, _args| Box::pin(std::future::pending()));
builtin.handles_signals_internally = true;
env.builtins.insert("foo", builtin);
let command: syntax::SimpleCommand = "foo".parse().unwrap();
let mut execute_fut = pin!(command.execute(&mut env));
assert_eq!(poll!(execute_fut.as_mut()), std::task::Poll::Pending);
system.raise(VirtualSystem::SIGINT).await.unwrap();
assert_eq!(poll!(execute_fut.as_mut()), std::task::Poll::Pending);
});
}
#[test]
fn xtrace_for_builtin() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("echo", echo_builtin());
env.options.set(yash_env::option::XTrace, On);
let command: syntax::SimpleCommand = "foo=bar echo hello >/dev/null".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "foo=bar echo hello 1>/dev/null\n");
});
}
}