Skip to main content

yash_semantics/command/simple_command/
function.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Simple command semantics for functions
18
19use super::perform_assignments;
20use crate::Handle;
21use crate::Runtime;
22use crate::redir::RedirGuard;
23use crate::xtrace::XTrace;
24use crate::xtrace::print;
25use crate::xtrace::trace_fields;
26use std::ops::ControlFlow::{Break, Continue};
27use std::pin::Pin;
28use std::rc::Rc;
29use yash_env::Env;
30use yash_env::function::Function;
31use yash_env::semantics::Divert;
32use yash_env::semantics::Field;
33use yash_env::semantics::Result;
34use yash_env::variable::Context;
35use yash_env::variable::PositionalParams;
36use yash_syntax::syntax::Assign;
37use yash_syntax::syntax::Redir;
38
39pub async fn execute_function<S: Runtime + 'static>(
40    env: &mut Env<S>,
41    function: Rc<Function<S>>,
42    assigns: &[Assign],
43    fields: Vec<Field>,
44    redirs: &[Redir],
45) -> Result {
46    let env = &mut RedirGuard::new(env);
47    let mut xtrace = XTrace::from_options(&env.options);
48    if let Err(e) = env.perform_redirs(redirs, xtrace.as_mut()).await {
49        return e.handle(env).await;
50    };
51
52    let mut env = env.push_context(Context::Volatile);
53    perform_assignments(&mut env, assigns, true, xtrace.as_mut()).await?;
54
55    trace_fields(xtrace.as_mut(), &fields);
56    print(&mut env, xtrace).await;
57
58    execute_function_body(&mut env, function, fields, None).await
59}
60
61type EnvPrepHook<S> = fn(&mut Env<S>) -> Pin<Box<dyn Future<Output = ()> + '_>>;
62
63/// Executes the body of the function.
64///
65/// The given function is executed in the given environment. The fields are
66/// passed as positional parameters to the function except for the first field
67/// which is the name of the function.
68///
69/// `env_prep_hook` is called after the new variable context is pushed to the
70/// environment. This is useful for assigning custom local variables before the
71/// function body is executed.
72pub async fn execute_function_body<S>(
73    env: &mut Env<S>,
74    function: Rc<Function<S>>,
75    fields: Vec<Field>,
76    env_prep_hook: Option<EnvPrepHook<S>>,
77) -> Result {
78    let positional_params = PositionalParams::from_fields(fields);
79    let mut env = env.push_context(Context::Regular { positional_params });
80    if let Some(hook) = env_prep_hook {
81        hook(&mut env).await;
82    }
83
84    // TODO Update control flow stack
85    let result = function.body.execute(&mut env).await;
86    if let Break(Divert::Return(exit_status)) = result {
87        if let Some(exit_status) = exit_status {
88            env.exit_status = exit_status;
89        }
90        Continue(())
91    } else {
92        result
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::command::Command as _;
100    use crate::command::function_definition::BodyImpl;
101    use crate::tests::echo_builtin;
102    use crate::tests::local_builtin;
103    use crate::tests::return_builtin;
104    use assert_matches::assert_matches;
105    use futures_util::FutureExt;
106    use std::rc::Rc;
107    use std::str::from_utf8;
108    use yash_env::VirtualSystem;
109    use yash_env::function::FunctionBodyObject;
110    use yash_env::option::State::On;
111    use yash_env::semantics::ExitStatus;
112    use yash_env::system::r#virtual::FileBody;
113    use yash_env::variable::Scope;
114    use yash_env_test_helper::assert_stderr;
115    use yash_env_test_helper::assert_stdout;
116    use yash_syntax::source::Location;
117    use yash_syntax::syntax::SimpleCommand;
118
119    fn function_body_impl(src: &str) -> Rc<dyn FunctionBodyObject<VirtualSystem>> {
120        Rc::new(BodyImpl(src.parse().unwrap()))
121    }
122
123    #[test]
124    fn simple_command_returns_exit_status_from_function() {
125        let mut env = Env::new_virtual();
126        env.builtins.insert("return", return_builtin());
127        let function = Function::new(
128            "foo",
129            function_body_impl("{ return -n 13; }"),
130            Location::dummy("dummy"),
131        );
132        env.functions.define(function).unwrap();
133        let command: SimpleCommand = "foo".parse().unwrap();
134
135        let result = command.execute(&mut env).now_or_never().unwrap();
136        assert_eq!(result, Continue(()));
137        assert_eq!(env.exit_status, ExitStatus(13));
138    }
139
140    #[test]
141    fn simple_command_applies_redirections_to_function() {
142        let system = VirtualSystem::new();
143        let state = Rc::clone(&system.state);
144        let mut env = Env::with_system(system);
145        env.builtins.insert("echo", echo_builtin());
146        let function = Function::new(
147            "foo",
148            function_body_impl("{ echo ok; }"),
149            Location::dummy("dummy"),
150        );
151        env.functions.define(function).unwrap();
152        let command: SimpleCommand = "foo >/tmp/file".parse().unwrap();
153
154        _ = command.execute(&mut env).now_or_never().unwrap();
155        let file = state.borrow().file_system.get("/tmp/file").unwrap();
156        let file = file.borrow();
157        assert_matches!(&file.body, FileBody::Regular { content, .. } => {
158            assert_eq!(from_utf8(content), Ok("ok\n"));
159        });
160    }
161
162    #[test]
163    fn simple_command_skips_running_function_on_redirection_error() {
164        let system = VirtualSystem::new();
165        let state = Rc::clone(&system.state);
166        let mut env = Env::with_system(system);
167        env.builtins.insert("echo", echo_builtin());
168        let function = Function::new(
169            "foo",
170            function_body_impl("{ echo ok; }"),
171            Location::dummy("dummy"),
172        );
173        env.functions.define(function).unwrap();
174        let command: SimpleCommand = "a=v foo </no/such/file".parse().unwrap();
175
176        let result = command.execute(&mut env).now_or_never().unwrap();
177        assert_eq!(result, Continue(()));
178        assert_eq!(env.exit_status, ExitStatus::ERROR);
179        assert_eq!(env.variables.get("a"), None);
180        assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
181    }
182
183    #[test]
184    fn function_call_consumes_return_without_exit_status() {
185        let mut env = Env::new_virtual();
186        env.builtins.insert("return", return_builtin());
187        let function = Function::new(
188            "foo",
189            function_body_impl("{ return; }"),
190            Location::dummy("dummy"),
191        );
192        env.functions.define(function).unwrap();
193        env.exit_status = ExitStatus(17);
194        let command: SimpleCommand = "foo".parse().unwrap();
195
196        let result = command.execute(&mut env).now_or_never().unwrap();
197        assert_eq!(result, Continue(()));
198        assert_eq!(env.exit_status, ExitStatus(17));
199    }
200
201    #[test]
202    fn function_call_consumes_return_with_exit_status() {
203        let mut env = Env::new_virtual();
204        env.builtins.insert("return", return_builtin());
205        let function = Function::new(
206            "foo",
207            function_body_impl("{ return 26; }"),
208            Location::dummy("dummy"),
209        );
210        env.functions.define(function).unwrap();
211        let command: SimpleCommand = "foo".parse().unwrap();
212
213        let result = command.execute(&mut env).now_or_never().unwrap();
214        assert_eq!(result, Continue(()));
215        assert_eq!(env.exit_status, ExitStatus(26));
216    }
217
218    #[test]
219    fn simple_command_passes_arguments_to_function() {
220        let system = VirtualSystem::new();
221        let state = Rc::clone(&system.state);
222        let mut env = Env::with_system(system);
223        env.builtins.insert("echo", echo_builtin());
224        let function = Function::new(
225            "foo",
226            function_body_impl("{ echo $1-$2-$3; }"),
227            Location::dummy("dummy"),
228        );
229        env.functions.define(function).unwrap();
230        let command: SimpleCommand = "foo bar baz".parse().unwrap();
231
232        let result = command.execute(&mut env).now_or_never().unwrap();
233        assert_eq!(result, Continue(()));
234        assert_stdout(&state, |stdout| assert_eq!(stdout, "bar-baz-\n"));
235    }
236
237    #[test]
238    fn simple_command_creates_temporary_context_executing_function() {
239        let system = VirtualSystem::new();
240        let state = Rc::clone(&system.state);
241        let mut env = Env::with_system(system);
242        env.builtins.insert("echo", echo_builtin());
243        env.builtins.insert("local", local_builtin());
244        let function = Function::new(
245            "foo",
246            function_body_impl("{ local x=42; echo $x; }"),
247            Location::dummy("dummy"),
248        );
249        env.functions.define(function).unwrap();
250        let command: SimpleCommand = "foo".parse().unwrap();
251
252        _ = command.execute(&mut env).now_or_never().unwrap();
253        assert_eq!(env.variables.get("x"), None);
254        assert_stdout(&state, |stdout| assert_eq!(stdout, "42\n"));
255    }
256
257    #[test]
258    fn simple_command_performs_function_assignment_in_temporary_context() {
259        let system = VirtualSystem::new();
260        let state = Rc::clone(&system.state);
261        let mut env = Env::with_system(system);
262        env.builtins.insert("echo", echo_builtin());
263        let function = Function::new(
264            "foo",
265            function_body_impl("{ echo $x; }"),
266            Location::dummy("dummy"),
267        );
268        env.functions.define(function).unwrap();
269        let command: SimpleCommand = "x=hello foo".parse().unwrap();
270
271        _ = command.execute(&mut env).now_or_never().unwrap();
272        assert_eq!(env.variables.get("x"), None);
273        assert_stdout(&state, |stdout| assert_eq!(stdout, "hello\n"));
274    }
275
276    #[test]
277    fn function_fails_on_reassigning_to_read_only_variable() {
278        let mut env = Env::new_virtual();
279        env.builtins.insert("echo", echo_builtin());
280        let function = Function::new(
281            "foo",
282            function_body_impl("{ echo; }"),
283            Location::dummy("dummy"),
284        );
285        env.functions.define(function).unwrap();
286        let mut var = env.variables.get_or_new("x", Scope::Global);
287        var.assign("", None).unwrap();
288        var.make_read_only(Location::dummy("readonly"));
289        let command: SimpleCommand = "x=hello foo".parse().unwrap();
290
291        let result = command.execute(&mut env).now_or_never().unwrap();
292        assert_matches!(result, Break(Divert::Interrupt(Some(exit_status))) => {
293            assert_ne!(exit_status, ExitStatus::SUCCESS);
294        });
295    }
296
297    #[test]
298    fn xtrace_for_function() {
299        let system = VirtualSystem::new();
300        let state = Rc::clone(&system.state);
301        let mut env = Env::with_system(system);
302        let function = Function::new(
303            "foo",
304            function_body_impl("for i in; do :; done"),
305            Location::dummy("dummy"),
306        );
307        env.functions.define(function).unwrap();
308        env.options.set(yash_env::option::XTrace, On);
309        let command: SimpleCommand = "x=hello foo bar <>/dev/null".parse().unwrap();
310
311        _ = command.execute(&mut env).now_or_never().unwrap();
312        assert_stderr(&state, |stderr| {
313            assert_eq!(stderr, "x=hello foo bar 0<>/dev/null\nfor i in\n");
314        });
315    }
316}