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