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 futures_util::FutureExt as _;
158    use std::assert_matches;
159    use std::ops::ControlFlow::{Break, Continue};
160    use std::pin::Pin;
161    use std::rc::Rc;
162    use yash_env::VirtualSystem;
163    use yash_env::builtin::Builtin;
164    use yash_env::builtin::Type::Special;
165    use yash_env::option::Option::ErrExit;
166    use yash_env::option::State::On;
167    use yash_env::semantics::Divert;
168    use yash_env::semantics::ExitStatus;
169    use yash_env::semantics::Field;
170    use yash_env::system::Concurrent;
171    use yash_env::system::r#virtual::FileBody;
172    use yash_env::test_helper::assert_stderr;
173    use yash_env::test_helper::assert_stdout;
174
175    #[test]
176    fn stack_in_condition() {
177        fn stub_builtin(
178            env: &mut Env<Rc<Concurrent<VirtualSystem>>>,
179            _args: Vec<Field>,
180        ) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
181            Box::pin(async move {
182                assert_matches!(
183                    env.stack.as_slice(),
184                    [Frame::Condition, Frame::Builtin { .. }]
185                );
186                Default::default()
187            })
188        }
189
190        let mut env = Env::new_virtual();
191        env.builtins
192            .insert("foo", Builtin::new(Special, stub_builtin));
193        let condition = "foo".parse().unwrap();
194
195        let result = evaluate_condition(&mut env, &condition)
196            .now_or_never()
197            .unwrap();
198        assert_eq!(result, Continue(true));
199    }
200
201    #[test]
202    fn redirecting_compound_command() {
203        let system = VirtualSystem::new();
204        let state = Rc::clone(&system.state);
205        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
206        env.builtins.insert("echo", echo_builtin());
207        let command: syntax::FullCompoundCommand = "{ echo 1; echo 2; } > /file".parse().unwrap();
208        let result = command.execute(&mut env).now_or_never().unwrap();
209        assert_eq!(result, Continue(()));
210        assert_eq!(env.exit_status, ExitStatus::SUCCESS);
211
212        let file = state.borrow().file_system.get("/file").unwrap();
213        let file = file.borrow();
214        assert_matches!(
215            &file.body,
216            FileBody::Regular { content, .. } if content == b"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}