Skip to main content

yash_semantics/
handle.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//! Error handlers.
18
19use crate::ExitStatus;
20use crate::expansion::ErrorCause;
21use std::ops::ControlFlow::{Break, Continue};
22use yash_env::Env;
23use yash_env::io::print_report;
24use yash_env::semantics::Divert;
25use yash_env::system::Isatty;
26use yash_env::system::concurrency::WriteAll;
27use yash_syntax::source::Source;
28
29/// Error handler.
30///
31/// Most errors in the shell are handled by printing an error message to the
32/// standard error and returning a non-zero exit status. This trait provides a
33/// standard interface for implementing that behavior.
34pub trait Handle<S> {
35    /// Handles the argument error.
36    #[allow(async_fn_in_trait, reason = "we don't support Send")]
37    async fn handle(&self, env: &mut Env<S>) -> super::Result;
38}
39
40/// Prints an error message.
41///
42/// This implementation handles the error by printing an error message to the
43/// standard error and returning `Divert::Interrupt(Some(exit_status))`, where
44/// `exit_status` is [`ExitStatus::ERROR`] if the error cause is a syntax error
45/// or the error location is [`Source::DotScript`], or
46/// [`ExitStatus::READ_ERROR`] otherwise.
47/// Note that other POSIX-compliant implementations may use different non-zero
48/// exit statuses instead of `ExitStatus::ERROR`.
49impl<S> Handle<S> for yash_syntax::parser::Error
50where
51    S: Isatty + WriteAll,
52{
53    async fn handle(&self, env: &mut Env<S>) -> super::Result {
54        print_report(env, &self.to_report()).await;
55
56        use yash_syntax::parser::ErrorCause::*;
57        let exit_status = match (&self.cause, &*self.location.code.source) {
58            (Syntax(_), _) | (Io(_), Source::DotScript { .. }) => ExitStatus::ERROR,
59            (Io(_), _) => ExitStatus::READ_ERROR,
60        };
61        Break(Divert::Interrupt(Some(exit_status)))
62    }
63}
64
65/// Prints an error message and returns a divert result indicating a non-zero
66/// exit status.
67///
68/// If the error cause is [`ErrorCause::Interrupted`], this implementation
69/// returns `Break(Divert::Interrupt(Some(exit_status)))` without printing an
70/// error message, where `exit_status` is the exit status contained in the
71/// error cause.
72///
73/// Otherwise, this implementation prints an error message to the standard
74/// error and returns `Divert::Interrupt(Some(ExitStatus::ERROR))`. If the
75/// [`ErrExit`] option is set, `Divert::Exit(Some(ExitStatus::ERROR))` is
76/// returned instead.
77///
78/// Note that other POSIX-compliant implementations may use different non-zero
79/// exit statuses.
80///
81/// [`ErrExit`]: yash_env::option::Option::ErrExit
82impl<S> Handle<S> for crate::expansion::Error
83where
84    S: Isatty + WriteAll,
85{
86    async fn handle(&self, env: &mut Env<S>) -> super::Result {
87        if let ErrorCause::Interrupted(exit_status) = &self.cause {
88            return Break(Divert::Interrupt(Some(*exit_status)));
89        }
90
91        print_report(env, &self.to_report()).await;
92
93        if env.errexit_is_applicable() {
94            Break(Divert::Exit(Some(ExitStatus::ERROR)))
95        } else {
96            Break(Divert::Interrupt(Some(ExitStatus::ERROR)))
97        }
98    }
99}
100
101/// Prints an error message and sets the exit status to non-zero.
102///
103/// This implementation handles a redirection error by printing an error message
104/// to the standard error and setting the exit status to [`ExitStatus::ERROR`].
105/// Note that other POSIX-compliant implementations may use different non-zero
106/// exit statuses.
107///
108/// This implementation does not return [`Divert::Interrupt`] because a
109/// redirection error does not always mean an interrupt. The shell should
110/// interrupt only on a redirection error during the execution of a special
111/// built-in. The caller is responsible for checking the condition and
112/// interrupting accordingly.
113impl<S> Handle<S> for crate::redir::Error
114where
115    S: Isatty + WriteAll,
116{
117    async fn handle(&self, env: &mut Env<S>) -> super::Result {
118        print_report(env, &self.to_report()).await;
119        env.exit_status = ExitStatus::ERROR;
120        Continue(())
121    }
122}
123
124#[cfg(test)]
125mod parser_error_tests {
126    use super::*;
127    use futures_util::FutureExt as _;
128    use yash_syntax::parser::{Error, ErrorCause, SyntaxError};
129    use yash_syntax::source::{Code, Location};
130
131    #[test]
132    fn handling_syntax_error() {
133        let mut env = Env::new_virtual();
134        let error = Error {
135            cause: ErrorCause::Syntax(SyntaxError::RedundantToken),
136            location: Location::dummy("test"),
137        };
138        let result = error.handle(&mut env).now_or_never().unwrap();
139        assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
140    }
141
142    #[test]
143    fn handling_io_error_in_command_file() {
144        let mut env = Env::new_virtual();
145        let code = Code {
146            value: "test".to_string().into(),
147            start_line_number: 1.try_into().unwrap(),
148            source: Source::CommandFile {
149                path: "test".to_string(),
150            }
151            .into(),
152        }
153        .into();
154        let range = 0..0;
155        let location = Location { code, range };
156        let cause = ErrorCause::Io(std::io::Error::other("error").into());
157        let error = Error { cause, location };
158
159        let result = error.handle(&mut env).now_or_never().unwrap();
160        assert_eq!(
161            result,
162            Break(Divert::Interrupt(Some(ExitStatus::READ_ERROR)))
163        );
164    }
165
166    #[test]
167    fn handling_io_error_in_dot_script() {
168        let mut env = Env::new_virtual();
169        let code = Code {
170            value: "test".to_string().into(),
171            start_line_number: 1.try_into().unwrap(),
172            source: Source::DotScript {
173                name: "test".to_string(),
174                origin: Location::dummy("test"),
175            }
176            .into(),
177        }
178        .into();
179        let range = 0..0;
180        let location = Location { code, range };
181        let cause = ErrorCause::Io(std::io::Error::other("error").into());
182        let error = Error { cause, location };
183
184        let result = error.handle(&mut env).now_or_never().unwrap();
185        assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::ERROR))));
186    }
187}