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 as _;
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 as _;
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::Concurrent;
113    use yash_env::system::r#virtual::FileBody;
114    use yash_env::test_helper::assert_stderr;
115    use yash_env::test_helper::assert_stdout;
116    use yash_env::variable::Scope;
117    use yash_syntax::source::Location;
118    use yash_syntax::syntax::SimpleCommand;
119
120    fn function_body_impl(src: &str) -> Rc<dyn FunctionBodyObject<Rc<Concurrent<VirtualSystem>>>> {
121        Rc::new(BodyImpl(src.parse().unwrap()))
122    }
123
124    #[test]
125    fn simple_command_returns_exit_status_from_function() {
126        let mut env = Env::new_virtual();
127        env.builtins.insert("return", return_builtin());
128        let function = Function::new(
129            "foo",
130            function_body_impl("{ return -n 13; }"),
131            Location::dummy("dummy"),
132        );
133        env.functions.define(function).unwrap();
134        let command: SimpleCommand = "foo".parse().unwrap();
135
136        let result = command.execute(&mut env).now_or_never().unwrap();
137        assert_eq!(result, Continue(()));
138        assert_eq!(env.exit_status, ExitStatus(13));
139    }
140
141    #[test]
142    fn simple_command_applies_redirections_to_function() {
143        let system = VirtualSystem::new();
144        let state = Rc::clone(&system.state);
145        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
146        env.builtins.insert("echo", echo_builtin());
147        let function = Function::new(
148            "foo",
149            function_body_impl("{ echo ok; }"),
150            Location::dummy("dummy"),
151        );
152        env.functions.define(function).unwrap();
153        let command: SimpleCommand = "foo >/tmp/file".parse().unwrap();
154
155        _ = command.execute(&mut env).now_or_never().unwrap();
156        let file = state.borrow().file_system.get("/tmp/file").unwrap();
157        let file = file.borrow();
158        assert_matches!(&file.body, FileBody::Regular { content, .. } => {
159            assert_eq!(from_utf8(content), Ok("ok\n"));
160        });
161    }
162
163    #[test]
164    fn simple_command_skips_running_function_on_redirection_error() {
165        let system = VirtualSystem::new();
166        let state = Rc::clone(&system.state);
167        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
168        env.builtins.insert("echo", echo_builtin());
169        let function = Function::new(
170            "foo",
171            function_body_impl("{ echo ok; }"),
172            Location::dummy("dummy"),
173        );
174        env.functions.define(function).unwrap();
175        let command: SimpleCommand = "a=v foo </no/such/file".parse().unwrap();
176
177        let result = command.execute(&mut env).now_or_never().unwrap();
178        assert_eq!(result, Continue(()));
179        assert_eq!(env.exit_status, ExitStatus::ERROR);
180        assert_eq!(env.variables.get("a"), None);
181        assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
182    }
183
184    #[test]
185    fn function_call_consumes_return_without_exit_status() {
186        let mut env = Env::new_virtual();
187        env.builtins.insert("return", return_builtin());
188        let function = Function::new(
189            "foo",
190            function_body_impl("{ return; }"),
191            Location::dummy("dummy"),
192        );
193        env.functions.define(function).unwrap();
194        env.exit_status = ExitStatus(17);
195        let command: SimpleCommand = "foo".parse().unwrap();
196
197        let result = command.execute(&mut env).now_or_never().unwrap();
198        assert_eq!(result, Continue(()));
199        assert_eq!(env.exit_status, ExitStatus(17));
200    }
201
202    #[test]
203    fn function_call_consumes_return_with_exit_status() {
204        let mut env = Env::new_virtual();
205        env.builtins.insert("return", return_builtin());
206        let function = Function::new(
207            "foo",
208            function_body_impl("{ return 26; }"),
209            Location::dummy("dummy"),
210        );
211        env.functions.define(function).unwrap();
212        let command: SimpleCommand = "foo".parse().unwrap();
213
214        let result = command.execute(&mut env).now_or_never().unwrap();
215        assert_eq!(result, Continue(()));
216        assert_eq!(env.exit_status, ExitStatus(26));
217    }
218
219    #[test]
220    fn simple_command_passes_arguments_to_function() {
221        let system = VirtualSystem::new();
222        let state = Rc::clone(&system.state);
223        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
224        env.builtins.insert("echo", echo_builtin());
225        let function = Function::new(
226            "foo",
227            function_body_impl("{ echo $1-$2-$3; }"),
228            Location::dummy("dummy"),
229        );
230        env.functions.define(function).unwrap();
231        let command: SimpleCommand = "foo bar baz".parse().unwrap();
232
233        let result = command.execute(&mut env).now_or_never().unwrap();
234        assert_eq!(result, Continue(()));
235        assert_stdout(&state, |stdout| assert_eq!(stdout, "bar-baz-\n"));
236    }
237
238    #[test]
239    fn simple_command_creates_temporary_context_executing_function() {
240        let system = VirtualSystem::new();
241        let state = Rc::clone(&system.state);
242        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
243        env.builtins.insert("echo", echo_builtin());
244        env.builtins.insert("local", local_builtin());
245        let function = Function::new(
246            "foo",
247            function_body_impl("{ local x=42; echo $x; }"),
248            Location::dummy("dummy"),
249        );
250        env.functions.define(function).unwrap();
251        let command: SimpleCommand = "foo".parse().unwrap();
252
253        _ = command.execute(&mut env).now_or_never().unwrap();
254        assert_eq!(env.variables.get("x"), None);
255        assert_stdout(&state, |stdout| assert_eq!(stdout, "42\n"));
256    }
257
258    #[test]
259    fn simple_command_performs_function_assignment_in_temporary_context() {
260        let system = VirtualSystem::new();
261        let state = Rc::clone(&system.state);
262        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
263        env.builtins.insert("echo", echo_builtin());
264        let function = Function::new(
265            "foo",
266            function_body_impl("{ echo $x; }"),
267            Location::dummy("dummy"),
268        );
269        env.functions.define(function).unwrap();
270        let command: SimpleCommand = "x=hello foo".parse().unwrap();
271
272        _ = command.execute(&mut env).now_or_never().unwrap();
273        assert_eq!(env.variables.get("x"), None);
274        assert_stdout(&state, |stdout| assert_eq!(stdout, "hello\n"));
275    }
276
277    #[test]
278    fn function_fails_on_reassigning_to_read_only_variable() {
279        let mut env = Env::new_virtual();
280        env.builtins.insert("echo", echo_builtin());
281        let function = Function::new(
282            "foo",
283            function_body_impl("{ echo; }"),
284            Location::dummy("dummy"),
285        );
286        env.functions.define(function).unwrap();
287        let mut var = env.variables.get_or_new("x", Scope::Global);
288        var.assign("", None).unwrap();
289        var.make_read_only(Location::dummy("readonly"));
290        let command: SimpleCommand = "x=hello foo".parse().unwrap();
291
292        let result = command.execute(&mut env).now_or_never().unwrap();
293        assert_matches!(result, Break(Divert::Interrupt(Some(exit_status))) => {
294            assert_ne!(exit_status, ExitStatus::SUCCESS);
295        });
296    }
297
298    #[test]
299    fn xtrace_for_function() {
300        let system = VirtualSystem::new();
301        let state = Rc::clone(&system.state);
302        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
303        let function = Function::new(
304            "foo",
305            function_body_impl("for i in; do :; done"),
306            Location::dummy("dummy"),
307        );
308        env.functions.define(function).unwrap();
309        env.options.set(yash_env::option::XTrace, On);
310        let command: SimpleCommand = "x=hello foo bar <>/dev/null".parse().unwrap();
311
312        _ = command.execute(&mut env).now_or_never().unwrap();
313        assert_stderr(&state, |stderr| {
314            assert_eq!(stderr, "x=hello foo bar 0<>/dev/null\nfor i in\n");
315        });
316    }
317}