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