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}