use super::perform_assignments;
use crate::Handle;
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 std::ops::ControlFlow::{Break, Continue};
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());
(builtin.execute)(env, fields).await
};
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;
use crate::tests::echo_builtin;
use crate::tests::local_builtin;
use crate::tests::return_builtin;
use assert_matches::assert_matches;
use futures_util::FutureExt;
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::State::On;
use yash_env::semantics::ExitStatus;
use yash_env::stack::Frame;
use yash_env::system::Errno;
use yash_env::system::Mode;
use yash_env::system::r#virtual::FileBody;
use yash_env::system::r#virtual::Inode;
use yash_env::test_helper::assert_stderr;
use yash_env::test_helper::assert_stdout;
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(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(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(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(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 system = VirtualSystem::new();
let mut env = Env::with_system(system);
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(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(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<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<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 xtrace_for_builtin() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(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");
});
}
}