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