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