use super::perform_assignments;
use crate::Handle as _;
use crate::Runtime;
use crate::redir::RedirGuard;
use crate::xtrace::XTrace;
use crate::xtrace::print;
use crate::xtrace::trace_fields;
use std::ops::ControlFlow::{Break, Continue};
use std::pin::Pin;
use std::rc::Rc;
use yash_env::Env;
use yash_env::function::Function;
use yash_env::semantics::Divert;
use yash_env::semantics::Field;
use yash_env::semantics::Result;
use yash_env::variable::Context;
use yash_env::variable::PositionalParams;
use yash_syntax::syntax::Assign;
use yash_syntax::syntax::Redir;
pub async fn execute_function<S: Runtime + 'static>(
env: &mut Env<S>,
function: Rc<Function<S>>,
assigns: &[Assign],
fields: Vec<Field>,
redirs: &[Redir],
) -> Result {
let env = &mut RedirGuard::new(env);
let mut xtrace = XTrace::from_options(&env.options);
if let Err(e) = env.perform_redirs(redirs, xtrace.as_mut()).await {
return e.handle(env).await;
};
let mut env = env.push_context(Context::Volatile);
perform_assignments(&mut env, assigns, true, xtrace.as_mut()).await?;
trace_fields(xtrace.as_mut(), &fields);
print(&mut env, xtrace).await;
execute_function_body(&mut env, function, fields, None).await
}
type EnvPrepHook<S> = fn(&mut Env<S>) -> Pin<Box<dyn Future<Output = ()> + '_>>;
pub async fn execute_function_body<S>(
env: &mut Env<S>,
function: Rc<Function<S>>,
fields: Vec<Field>,
env_prep_hook: Option<EnvPrepHook<S>>,
) -> Result {
let positional_params = PositionalParams::from_fields(fields);
let mut env = env.push_context(Context::Regular { positional_params });
if let Some(hook) = env_prep_hook {
hook(&mut env).await;
}
let result = function.body.execute(&mut env).await;
if let Break(Divert::Return(exit_status)) = result {
if let Some(exit_status) = exit_status {
env.exit_status = exit_status;
}
Continue(())
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::Command as _;
use crate::command::function_definition::BodyImpl;
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 std::rc::Rc;
use std::str::from_utf8;
use yash_env::VirtualSystem;
use yash_env::function::FunctionBodyObject;
use yash_env::option::State::On;
use yash_env::semantics::ExitStatus;
use yash_env::system::Concurrent;
use yash_env::system::r#virtual::FileBody;
use yash_env::test_helper::assert_stderr;
use yash_env::test_helper::assert_stdout;
use yash_env::variable::Scope;
use yash_syntax::source::Location;
use yash_syntax::syntax::SimpleCommand;
fn function_body_impl(src: &str) -> Rc<dyn FunctionBodyObject<Rc<Concurrent<VirtualSystem>>>> {
Rc::new(BodyImpl(src.parse().unwrap()))
}
#[test]
fn simple_command_returns_exit_status_from_function() {
let mut env = Env::new_virtual();
env.builtins.insert("return", return_builtin());
let function = Function::new(
"foo",
function_body_impl("{ return -n 13; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "foo".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus(13));
}
#[test]
fn simple_command_applies_redirections_to_function() {
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 function = Function::new(
"foo",
function_body_impl("{ echo ok; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "foo >/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("ok\n"));
});
}
#[test]
fn simple_command_skips_running_function_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 function = Function::new(
"foo",
function_body_impl("{ echo ok; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "a=v foo </no/such/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!(env.variables.get("a"), None);
assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn function_call_consumes_return_without_exit_status() {
let mut env = Env::new_virtual();
env.builtins.insert("return", return_builtin());
let function = Function::new(
"foo",
function_body_impl("{ return; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
env.exit_status = ExitStatus(17);
let command: SimpleCommand = "foo".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus(17));
}
#[test]
fn function_call_consumes_return_with_exit_status() {
let mut env = Env::new_virtual();
env.builtins.insert("return", return_builtin());
let function = Function::new(
"foo",
function_body_impl("{ return 26; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "foo".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_eq!(env.exit_status, ExitStatus(26));
}
#[test]
fn simple_command_passes_arguments_to_function() {
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 function = Function::new(
"foo",
function_body_impl("{ echo $1-$2-$3; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "foo bar baz".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&state, |stdout| assert_eq!(stdout, "bar-baz-\n"));
}
#[test]
fn simple_command_creates_temporary_context_executing_function() {
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("local", local_builtin());
let function = Function::new(
"foo",
function_body_impl("{ local x=42; echo $x; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "foo".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.variables.get("x"), None);
assert_stdout(&state, |stdout| assert_eq!(stdout, "42\n"));
}
#[test]
fn simple_command_performs_function_assignment_in_temporary_context() {
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 function = Function::new(
"foo",
function_body_impl("{ echo $x; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let command: SimpleCommand = "x=hello foo".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_eq!(env.variables.get("x"), None);
assert_stdout(&state, |stdout| assert_eq!(stdout, "hello\n"));
}
#[test]
fn function_fails_on_reassigning_to_read_only_variable() {
let mut env = Env::new_virtual();
env.builtins.insert("echo", echo_builtin());
let function = Function::new(
"foo",
function_body_impl("{ echo; }"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
let mut var = env.variables.get_or_new("x", Scope::Global);
var.assign("", None).unwrap();
var.make_read_only(Location::dummy("readonly"));
let command: SimpleCommand = "x=hello foo".parse().unwrap();
let result = command.execute(&mut env).now_or_never().unwrap();
assert_matches!(result, Break(Divert::Interrupt(Some(exit_status))) => {
assert_ne!(exit_status, ExitStatus::SUCCESS);
});
}
#[test]
fn xtrace_for_function() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
let function = Function::new(
"foo",
function_body_impl("for i in; do :; done"),
Location::dummy("dummy"),
);
env.functions.define(function).unwrap();
env.options.set(yash_env::option::XTrace, On);
let command: SimpleCommand = "x=hello foo bar <>/dev/null".parse().unwrap();
_ = command.execute(&mut env).now_or_never().unwrap();
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "x=hello foo bar 0<>/dev/null\nfor i in\n");
});
}
}