use crate::command::Command as _;
use crate::trap::run_traps_for_caught_signals;
use crate::{Handle as _, Runtime};
use std::cell::RefCell;
use std::ops::ControlFlow::{Break, Continue};
use yash_env::Env;
use yash_env::semantics::Divert;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Result;
use yash_syntax::parser::lex::Lexer;
use yash_syntax::parser::{ErrorCause, Parser};
use yash_syntax::syntax::List;
pub async fn read_eval_loop<S: Runtime + 'static>(
env: &RefCell<&mut Env<S>>,
lexer: &mut Lexer<'_>,
) -> Result {
read_eval_loop_impl(env, lexer, false).await
}
pub async fn interactive_read_eval_loop<S: Runtime + 'static>(
env: &RefCell<&mut Env<S>>,
lexer: &mut Lexer<'_>,
) -> Result {
read_eval_loop_impl(env, lexer, true).await
}
#[allow(
clippy::await_holding_refcell_ref,
reason = "the parser does not run concurrently with the executor"
)]
async fn read_eval_loop_impl<S: Runtime + 'static>(
env: &RefCell<&mut Env<S>>,
lexer: &mut Lexer<'_>,
is_interactive: bool,
) -> Result {
let mut executed = false;
loop {
if !lexer.pending() {
lexer.flush();
}
let command = Parser::config()
.aliases(env)
.declaration_utilities(env)
.input(lexer)
.command_line()
.await;
let env = &mut **env.borrow_mut();
let (mut result, error_recoverable) = match command {
Ok(None) => {
if !executed {
env.exit_status = ExitStatus::SUCCESS;
}
return Continue(());
}
Ok(Some(command)) => (run_command(env, &command).await, true),
Err(error) => {
let result = error.handle(env).await;
let error_recoverable = matches!(error.cause, ErrorCause::Syntax(_));
(result, error_recoverable)
}
};
if is_interactive && error_recoverable {
if let Break(Divert::Interrupt(exit_status)) = result {
if let Some(exit_status) = exit_status {
env.exit_status = exit_status;
}
result = Continue(());
lexer.flush();
}
}
result?;
executed = true;
}
}
async fn run_command<S: Runtime + 'static>(env: &mut Env<S>, command: &List) -> Result {
run_traps_for_caught_signals(env).await?;
env.update_all_subshell_statuses();
command.execute(env).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::echo_builtin;
use crate::tests::return_builtin;
use futures_util::FutureExt as _;
use std::rc::Rc;
use yash_env::input::Echo;
use yash_env::input::Memory;
use yash_env::option::Option::Verbose;
use yash_env::option::State::On;
use yash_env::system::Concurrent;
use yash_env::system::r#virtual::SIGUSR1;
use yash_env::system::r#virtual::VirtualSystem;
use yash_env::test_helper::assert_stderr;
use yash_env::test_helper::assert_stdout;
use yash_env::trap::Action;
use yash_syntax::input::Context;
use yash_syntax::source::Location;
#[test]
fn exit_status_zero_with_no_commands() {
let mut env = Env::new_virtual();
env.exit_status = ExitStatus(5);
let mut lexer = Lexer::with_code("");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
}
#[test]
fn exit_status_in_out() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.exit_status = ExitStatus(42);
env.builtins.insert("echo", echo_builtin());
env.builtins.insert("return", return_builtin());
let mut lexer = Lexer::with_code("echo $?; return -n 7");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus(7));
assert_stdout(&state, |stdout| assert_eq!(stdout, "42\n"));
}
#[test]
fn executing_many_lines_of_code() {
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 mut lexer = Lexer::with_code("echo 1\necho 2\necho 3;");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&state, |stdout| assert_eq!(stdout, "1\n2\n3\n"));
}
#[test]
fn parsing_with_aliases() {
use yash_syntax::alias::{Alias, HashEntry};
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.aliases.insert(HashEntry(Rc::new(Alias {
name: "echo".to_string(),
replacement: "echo alias\necho ok".to_string(),
global: false,
origin: Location::dummy(""),
})));
env.builtins.insert("echo", echo_builtin());
let mut lexer = Lexer::with_code("echo");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
assert_stdout(&state, |stdout| assert_eq!(stdout, "alias\nok\n"));
}
#[test]
fn verbose_option() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Verbose, On);
let ref_env = RefCell::new(&mut env);
let input = Box::new(Echo::new(Memory::new("case _ in esac"), &ref_env));
let mut lexer = Lexer::new(input);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
drop(lexer);
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
assert_stderr(&state, |stderr| assert_eq!(stderr, "case _ in esac"));
}
#[test]
fn command_interrupt_interactive() {
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 mut lexer = Lexer::with_code("${X?}\necho $?\n");
let ref_env = RefCell::new(&mut env);
let result = interactive_read_eval_loop(&ref_env, &mut lexer)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&state, |stdout| assert_eq!(stdout, "2\n"));
}
#[test]
fn command_other_divert_interactive() {
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("return", return_builtin());
let mut lexer = Lexer::with_code("return 123\necho $?\n");
let ref_env = RefCell::new(&mut env);
let result = interactive_read_eval_loop(&ref_env, &mut lexer)
.now_or_never()
.unwrap();
assert_eq!(result, Break(Divert::Return(Some(ExitStatus(123)))));
assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn command_interrupt_non_interactive() {
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 mut lexer = Lexer::with_code("${X?}\necho $?\n");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn handling_syntax_error() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
let mut lexer = Lexer::with_code(";;");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
#[test]
fn syntax_error_aborts_non_interactive_loop() {
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 mut lexer = Lexer::with_code(";;\necho !");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn syntax_error_continues_interactive_loop() {
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 mut lexer = Lexer::with_code(";; (\necho $?");
let ref_env = RefCell::new(&mut env);
let result = interactive_read_eval_loop(&ref_env, &mut lexer)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&state, |stdout| assert_eq!(stdout, "2\n"));
}
#[test]
fn input_error_aborts_loop() {
struct BrokenInput;
impl yash_syntax::input::Input for BrokenInput {
async fn next_line(&mut self, _context: &Context) -> std::io::Result<String> {
Err(std::io::Error::other("broken"))
}
}
let mut lexer = Lexer::new(Box::new(BrokenInput));
let mut env = Env::new_virtual();
let ref_env = RefCell::new(&mut env);
let result = interactive_read_eval_loop(&ref_env, &mut lexer)
.now_or_never()
.unwrap();
assert_eq!(
result,
Break(Divert::Interrupt(Some(ExitStatus::READ_ERROR)))
);
}
#[test]
fn running_traps_between_parsing_and_executing() {
let system = VirtualSystem::new();
let pid = system.process_id;
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.builtins.insert("echo", echo_builtin());
env.traps
.set_action(
&env.system,
SIGUSR1,
Action::Command("echo USR1".into()),
Location::dummy(""),
false,
)
.now_or_never()
.unwrap()
.unwrap();
let _ = state
.borrow_mut()
.processes
.get_mut(&pid)
.unwrap()
.raise_signal(SIGUSR1);
let mut lexer = Lexer::with_code("echo $?");
let ref_env = RefCell::new(&mut env);
let result = read_eval_loop(&ref_env, &mut lexer).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus::SUCCESS);
assert_stdout(&state, |stdout| assert_eq!(stdout, "USR1\n0\n"));
}
}