Skip to main content

yash_semantics/command/
compound_command.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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//! Implementation of the compound command semantics.
18
19use super::Command;
20use crate::Handle;
21use crate::Runtime;
22use crate::redir::RedirGuard;
23use crate::xtrace::XTrace;
24use crate::xtrace::finish;
25use std::ops::ControlFlow::Continue;
26use yash_env::Env;
27use yash_env::semantics::ExitStatus;
28use yash_env::semantics::Result;
29use yash_env::stack::Frame;
30#[cfg(doc)]
31use yash_env::subshell::Subshell;
32use yash_syntax::syntax;
33use yash_syntax::syntax::Redir;
34
35/// Performs redirections, printing their trace if required.
36async fn perform_redirs<S: Runtime + 'static>(
37    env: &mut RedirGuard<'_, S>,
38    redirs: &[Redir],
39) -> std::result::Result<Option<ExitStatus>, crate::redir::Error> {
40    let mut xtrace = XTrace::from_options(&env.options);
41    let result = env.perform_redirs(redirs, xtrace.as_mut()).await;
42    let xtrace = finish(env, xtrace).await;
43    env.system.print_error(&xtrace).await;
44    result
45}
46
47/// Executes the condition of an if/while/until command.
48async fn evaluate_condition<S: Runtime + 'static>(
49    env: &mut Env<S>,
50    condition: &syntax::List,
51) -> Result<bool> {
52    let mut env = env.push_frame(Frame::Condition);
53    condition.execute(&mut env).await?;
54    Continue(env.exit_status.is_successful())
55}
56
57mod case;
58mod for_loop;
59mod r#if;
60mod subshell;
61mod while_loop;
62
63/// Executes the compound command.
64///
65/// The redirections are performed, if any, before executing the command body.
66/// Redirection errors are subject to the `ErrExit` option
67/// (`Env::apply_errexit`).
68impl<S: Runtime + 'static> Command<S> for syntax::FullCompoundCommand {
69    async fn execute(&self, env: &mut Env<S>) -> Result {
70        let mut env = RedirGuard::new(env);
71        match perform_redirs(&mut env, &self.redirs).await {
72            Ok(_) => self.command.execute(&mut env).await,
73            Err(error) => {
74                error.handle(&mut env).await?;
75                env.apply_errexit()
76            }
77        }
78    }
79}
80
81/// Executes the compound command.
82///
83/// # Grouping
84///
85/// A grouping is executed by running the contained list.
86///
87/// # Subshell
88///
89/// A subshell is executed by running the contained list in a separate
90/// environment ([`Subshell`]).
91///
92/// After the subshell has finished, [`Env::apply_errexit`] is called.
93///
94/// # For loop
95///
96/// Executing a for loop starts with expanding the `name` and `values`. If
97/// `values` is `None`, it expands to the current positional parameters. Each
98/// field resulting from the expansion is assigned to the variable `name`, and
99/// in turn, `body` is executed.
100///
101/// # While loop
102///
103/// The `condition` is executed first. If its exit status is zero, the `body` is
104/// executed. The execution is repeated while the `condition` exit status is
105/// zero.
106///
107/// # Until loop
108///
109/// The until loop is executed in the same manner as the while loop except that
110/// the loop condition is inverted: The execution continues until the
111/// `condition` exit status is zero.
112///
113/// # If conditional construct
114///
115/// The if command first executes the `condition`. If its exit status is zero,
116/// it runs the `body`, and its exit status becomes that of the if command.
117/// Otherwise, it executes the `condition` of each elif-then clause until
118/// finding a condition that returns an exit status of zero, after which it runs
119/// the corresponding `body`. If all the conditions result in a non-zero exit
120/// status, it runs the `else` clause, if any. In case the command has no `else`
121/// clause, the final exit status will be zero.
122///
123/// # Case conditional construct
124///
125/// The "case" command expands the subject word and executes the body of the
126/// first item with a pattern matching the word. Each pattern is subjected to
127/// word expansion before matching.
128///
129/// POSIX does not specify the order in which the shell tests multiple patterns
130/// in an item. This implementation tries them in the order of appearance.
131///
132/// After executing the body of the matching item, the case command may process
133/// the next item depending on the continuation.
134impl<S: Runtime + 'static> Command<S> for syntax::CompoundCommand {
135    async fn execute(&self, env: &mut Env<S>) -> Result {
136        use syntax::CompoundCommand::*;
137        match self {
138            Grouping(list) => list.execute(env).await,
139            Subshell { body, location } => subshell::execute(env, body.clone(), location).await,
140            For { name, values, body } => for_loop::execute(env, name, values, body).await,
141            While { condition, body } => while_loop::execute_while(env, condition, body).await,
142            Until { condition, body } => while_loop::execute_until(env, condition, body).await,
143            If {
144                condition,
145                body,
146                elifs,
147                r#else,
148            } => r#if::execute(env, condition, body, elifs, r#else).await,
149            Case { subject, items } => case::execute(env, subject, items).await,
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::tests::echo_builtin;
158    use crate::tests::return_builtin;
159    use assert_matches::assert_matches;
160    use futures_util::FutureExt;
161    use std::ops::ControlFlow::{Break, Continue};
162    use std::pin::Pin;
163    use std::rc::Rc;
164    use std::str::from_utf8;
165    use yash_env::VirtualSystem;
166    use yash_env::builtin::Builtin;
167    use yash_env::builtin::Type::Special;
168    use yash_env::option::Option::ErrExit;
169    use yash_env::option::State::On;
170    use yash_env::semantics::Divert;
171    use yash_env::semantics::ExitStatus;
172    use yash_env::semantics::Field;
173    use yash_env::system::r#virtual::FileBody;
174    use yash_env_test_helper::assert_stderr;
175    use yash_env_test_helper::assert_stdout;
176
177    #[test]
178    fn stack_in_condition() {
179        fn stub_builtin(
180            env: &mut Env<VirtualSystem>,
181            _args: Vec<Field>,
182        ) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
183            Box::pin(async move {
184                assert_matches!(
185                    env.stack.as_slice(),
186                    [Frame::Condition, Frame::Builtin { .. }]
187                );
188                Default::default()
189            })
190        }
191
192        let mut env = Env::new_virtual();
193        env.builtins
194            .insert("foo", Builtin::new(Special, stub_builtin));
195        let condition = "foo".parse().unwrap();
196
197        let result = evaluate_condition(&mut env, &condition)
198            .now_or_never()
199            .unwrap();
200        assert_eq!(result, Continue(true));
201    }
202
203    #[test]
204    fn redirecting_compound_command() {
205        let system = VirtualSystem::new();
206        let state = Rc::clone(&system.state);
207        let mut env = Env::with_system(system);
208        env.builtins.insert("echo", echo_builtin());
209        let command: syntax::FullCompoundCommand = "{ echo 1; echo 2; } > /file".parse().unwrap();
210        let result = command.execute(&mut env).now_or_never().unwrap();
211        assert_eq!(result, Continue(()));
212        assert_eq!(env.exit_status, ExitStatus::SUCCESS);
213
214        let file = state.borrow().file_system.get("/file").unwrap();
215        let file = file.borrow();
216        assert_matches!(&file.body, FileBody::Regular { content, .. } => {
217            assert_eq!(from_utf8(content).unwrap(), "1\n2\n");
218        });
219    }
220
221    #[test]
222    fn tracing_redirections() {
223        let system = VirtualSystem::new();
224        let state = Rc::clone(&system.state);
225        let mut env = Env::with_system(system);
226        env.builtins.insert("echo", echo_builtin());
227        env.options.set(yash_env::option::Option::XTrace, On);
228        let command: syntax::FullCompoundCommand = "{ echo X; } > /file < /file".parse().unwrap();
229        let _ = command.execute(&mut env).now_or_never().unwrap();
230        assert_stderr(&state, |stderr| {
231            assert_eq!(stderr, "1>/file 0</file\necho X\n");
232        });
233    }
234
235    #[test]
236    fn redirection_error_prevents_command_execution() {
237        let system = VirtualSystem::new();
238        let state = Rc::clone(&system.state);
239        let mut env = Env::with_system(system);
240        env.builtins.insert("echo", echo_builtin());
241        let command: syntax::FullCompoundCommand =
242            "{ echo not reached; } < /no/such/file".parse().unwrap();
243        let result = command.execute(&mut env).now_or_never().unwrap();
244        assert_eq!(result, Continue(()));
245        assert_eq!(env.exit_status, ExitStatus::ERROR);
246        assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
247    }
248
249    #[test]
250    fn redirection_error_triggers_errexit() {
251        let mut env = Env::new_virtual();
252        env.builtins.insert("echo", echo_builtin());
253        env.options.set(ErrExit, On);
254        let command: syntax::FullCompoundCommand =
255            "{ echo not reached; } < /no/such/file".parse().unwrap();
256        let result = command.execute(&mut env).now_or_never().unwrap();
257        assert_eq!(result, Break(Divert::Exit(None)));
258        assert_eq!(env.exit_status, ExitStatus::ERROR);
259    }
260
261    #[test]
262    fn grouping_executes_list() {
263        let mut env = Env::new_virtual();
264        env.builtins.insert("return", return_builtin());
265        let command: syntax::CompoundCommand = "{ return -n 42; }".parse().unwrap();
266        let result = command.execute(&mut env).now_or_never().unwrap();
267        assert_eq!(result, Continue(()));
268        assert_eq!(env.exit_status, ExitStatus(42));
269    }
270}