Skip to main content

yash_env/input/
ignore_eof.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 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//! Defines the [`IgnoreEof`] input decorator.
18
19use super::{Context, Input, Result};
20use crate::Env;
21use crate::io::Fd;
22use crate::option::{IgnoreEof as IgnoreEofOption, Interactive, Off};
23use crate::system::Isatty;
24use crate::system::concurrency::WriteAll;
25use std::cell::RefCell;
26
27/// `Input` decorator that ignores EOF on a terminal
28///
29/// This is a decorator of [`Input`] that adds the behavior of the
30/// [`ignore-eof` shell option](crate::option::IgnoreEof).
31///
32/// The decorator is effective only when all of the following conditions are
33/// met:
34///
35/// - The shell is interactive, that is, the [`Interactive`] option is enabled.
36/// - The `ignore-eof` option is enabled.
37/// - The input is a terminal.
38///
39/// The decorator reads from the inner input and usually returns the result
40/// as is. However, if the result is an empty string and the above conditions
41/// are met, the decorator will re-read the input until a non-empty string
42/// is obtained, an error occurs, or this process is repeated 50 times.
43///
44/// [`Interactive`]: crate::option::Interactive
45///
46/// For the top-level interactive read loop, prefer [`EofGuard`] over this
47/// decorator. [`EofGuard`] also handles the case where there are suspended
48/// jobs, which this decorator does not.
49///
50/// [`EofGuard`]: crate::input::EofGuard
51#[derive(Debug)]
52pub struct IgnoreEof<'a, 'b, S, T> {
53    /// Inner input to read from
54    inner: T,
55    /// File descriptor to be checked if it is a terminal
56    fd: Fd,
57    /// Environment to check the shell options and interact with the system
58    env: &'a RefCell<&'b mut Env<S>>,
59    /// Text to be displayed when EOF is ignored
60    message: String,
61}
62
63impl<'a, 'b, S, T> IgnoreEof<'a, 'b, S, T> {
64    /// Creates a new `IgnoreEof` decorator.
65    ///
66    /// The first argument is the inner `Input` that performs the actual input
67    /// operation. The second argument is the file descriptor to be checked if
68    /// it is a terminal. The third argument is the shell environment that
69    /// contains the shell option state and the system interface to interact
70    /// with the system.  It is wrapped in a `RefCell` so that it can be shared
71    /// with other decorators and the parser. The fourth argument is the text to
72    /// be displayed when EOF is ignored.
73    ///
74    /// The second argument `fd` should match the file descriptor that the inner
75    /// input reads from. If the inner input reads from a different file
76    /// descriptor, the `IgnoreEof` decorator may not detect the terminal
77    /// correctly.
78    pub fn new(inner: T, fd: Fd, env: &'a RefCell<&'b mut Env<S>>, message: String) -> Self {
79        Self {
80            inner,
81            fd,
82            env,
83            message,
84        }
85    }
86}
87
88// Not derived automatically because S may not implement Clone.
89impl<S, T: Clone> Clone for IgnoreEof<'_, '_, S, T> {
90    fn clone(&self) -> Self {
91        Self {
92            inner: self.inner.clone(),
93            fd: self.fd,
94            env: self.env,
95            message: self.message.clone(),
96        }
97    }
98}
99
100impl<S: Isatty + WriteAll, T: Input> Input for IgnoreEof<'_, '_, S, T> {
101    #[allow(
102        clippy::await_holding_refcell_ref,
103        reason = "other decorators, the parser, or the executor do not run concurrently with this method"
104    )]
105    async fn next_line(&mut self, context: &Context) -> Result {
106        let mut remaining_tries = 50;
107
108        loop {
109            let line = self.inner.next_line(context).await?;
110
111            let env = self.env.borrow();
112
113            let should_break = !line.is_empty()
114                || env.options.get(Interactive) == Off
115                || env.options.get(IgnoreEofOption) == Off
116                || remaining_tries == 0
117                || !env.system.isatty(self.fd);
118            if should_break {
119                return Ok(line);
120            }
121
122            env.system.print_error(&self.message).await;
123            remaining_tries -= 1;
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::super::Memory;
131    use super::*;
132    use crate::option::On;
133    use crate::system::r#virtual::{FdBody, FileBody, Inode, OpenFileDescription, VirtualSystem};
134    use crate::system::{Concurrent, Mode};
135    use crate::test_helper::assert_stderr;
136    use enumset::EnumSet;
137    use futures_util::FutureExt as _;
138    use std::rc::Rc;
139
140    /// `Input` decorator that returns EOF for the first `count` calls
141    /// and then reads from the inner input.
142    struct EofStub<T> {
143        inner: T,
144        count: usize,
145    }
146
147    impl<T> Input for EofStub<T>
148    where
149        T: Input,
150    {
151        async fn next_line(&mut self, context: &Context) -> Result {
152            if let Some(remaining) = self.count.checked_sub(1) {
153                self.count = remaining;
154                Ok("".to_string())
155            } else {
156                self.inner.next_line(context).await
157            }
158        }
159    }
160
161    fn set_stdin_to_tty(system: &mut VirtualSystem) {
162        system
163            .current_process_mut()
164            .set_fd(
165                Fd::STDIN,
166                FdBody {
167                    open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
168                        Rc::new(RefCell::new(Inode {
169                            body: FileBody::Terminal { content: vec![] },
170                            permissions: Mode::empty(),
171                        })),
172                        /* offset = */ 0,
173                        /* is_readable = */ true,
174                        /* is_writable = */ true,
175                        /* is_appending = */ false,
176                        /* is_nonblocking = */ false,
177                    ))),
178                    flags: EnumSet::empty(),
179                },
180            )
181            .unwrap();
182    }
183
184    fn set_stdin_to_regular_file(system: &mut VirtualSystem) {
185        system
186            .current_process_mut()
187            .set_fd(
188                Fd::STDIN,
189                FdBody {
190                    open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
191                        Rc::new(RefCell::new(Inode {
192                            body: FileBody::Regular {
193                                content: vec![],
194                                is_native_executable: false,
195                            },
196                            permissions: Mode::empty(),
197                        })),
198                        /* offset = */ 0,
199                        /* is_readable = */ true,
200                        /* is_writable = */ true,
201                        /* is_appending = */ false,
202                        /* is_nonblocking = */ false,
203                    ))),
204                    flags: EnumSet::empty(),
205                },
206            )
207            .unwrap();
208    }
209
210    #[test]
211    fn decorator_reads_from_inner_input() {
212        let mut system = VirtualSystem::new();
213        set_stdin_to_tty(&mut system);
214        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
215        env.options.set(Interactive, On);
216        env.options.set(IgnoreEofOption, On);
217        let ref_env = RefCell::new(&mut env);
218        let mut decorator = IgnoreEof::new(
219            Memory::new("echo foo\n"),
220            Fd::STDIN,
221            &ref_env,
222            "unused".to_string(),
223        );
224
225        let result = decorator
226            .next_line(&Context::default())
227            .now_or_never()
228            .unwrap();
229        assert_eq!(result.unwrap(), "echo foo\n");
230    }
231
232    #[test]
233    fn decorator_reads_input_again_on_eof() {
234        let mut system = VirtualSystem::new();
235        set_stdin_to_tty(&mut system);
236        let state = system.state.clone();
237        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
238        env.options.set(Interactive, On);
239        env.options.set(IgnoreEofOption, On);
240        let ref_env = RefCell::new(&mut env);
241        let mut decorator = IgnoreEof::new(
242            EofStub {
243                inner: Memory::new("echo foo\n"),
244                count: 1,
245            },
246            Fd::STDIN,
247            &ref_env,
248            "EOF ignored\n".to_string(),
249        );
250
251        let result = decorator
252            .next_line(&Context::default())
253            .now_or_never()
254            .unwrap();
255        assert_eq!(result.unwrap(), "echo foo\n");
256        assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
257    }
258
259    #[test]
260    fn decorator_reads_input_up_to_50_times() {
261        let mut system = VirtualSystem::new();
262        set_stdin_to_tty(&mut system);
263        let state = system.state.clone();
264        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
265        env.options.set(Interactive, On);
266        env.options.set(IgnoreEofOption, On);
267        let ref_env = RefCell::new(&mut env);
268        let mut decorator = IgnoreEof::new(
269            EofStub {
270                inner: Memory::new("echo foo\n"),
271                count: 50,
272            },
273            Fd::STDIN,
274            &ref_env,
275            "EOF ignored\n".to_string(),
276        );
277
278        let result = decorator
279            .next_line(&Context::default())
280            .now_or_never()
281            .unwrap();
282        assert_eq!(result.unwrap(), "echo foo\n");
283        assert_stderr(&state, |stderr| {
284            assert_eq!(stderr, "EOF ignored\n".repeat(50))
285        });
286    }
287
288    #[test]
289    fn decorator_returns_empty_line_after_reading_51_times() {
290        let mut system = VirtualSystem::new();
291        set_stdin_to_tty(&mut system);
292        let state = system.state.clone();
293        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
294        env.options.set(Interactive, On);
295        env.options.set(IgnoreEofOption, On);
296        let ref_env = RefCell::new(&mut env);
297        let mut decorator = IgnoreEof::new(
298            EofStub {
299                inner: Memory::new("echo foo\n"),
300                count: 51,
301            },
302            Fd::STDIN,
303            &ref_env,
304            "EOF ignored\n".to_string(),
305        );
306
307        let result = decorator
308            .next_line(&Context::default())
309            .now_or_never()
310            .unwrap();
311        assert_eq!(result.unwrap(), "");
312        assert_stderr(&state, |stderr| {
313            assert_eq!(stderr, "EOF ignored\n".repeat(50))
314        });
315    }
316
317    #[test]
318    fn decorator_returns_immediately_if_not_interactive() {
319        let mut system = VirtualSystem::new();
320        set_stdin_to_tty(&mut system);
321        let state = system.state.clone();
322        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
323        env.options.set(Interactive, Off);
324        env.options.set(IgnoreEofOption, On);
325        let ref_env = RefCell::new(&mut env);
326        let mut decorator = IgnoreEof::new(
327            EofStub {
328                inner: Memory::new("echo foo\n"),
329                count: 1,
330            },
331            Fd::STDIN,
332            &ref_env,
333            "EOF ignored\n".to_string(),
334        );
335
336        let result = decorator
337            .next_line(&Context::default())
338            .now_or_never()
339            .unwrap();
340        assert_eq!(result.unwrap(), "");
341        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
342    }
343
344    #[test]
345    fn decorator_returns_immediately_if_not_ignore_eof() {
346        let mut system = VirtualSystem::new();
347        set_stdin_to_tty(&mut system);
348        let state = system.state.clone();
349        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
350        env.options.set(Interactive, On);
351        env.options.set(IgnoreEofOption, Off);
352        let ref_env = RefCell::new(&mut env);
353        let mut decorator = IgnoreEof::new(
354            EofStub {
355                inner: Memory::new("echo foo\n"),
356                count: 1,
357            },
358            Fd::STDIN,
359            &ref_env,
360            "EOF ignored\n".to_string(),
361        );
362
363        let result = decorator
364            .next_line(&Context::default())
365            .now_or_never()
366            .unwrap();
367        assert_eq!(result.unwrap(), "");
368        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
369    }
370
371    #[test]
372    fn decorator_returns_immediately_if_not_terminal() {
373        let mut system = VirtualSystem::new();
374        set_stdin_to_regular_file(&mut system);
375        let state = system.state.clone();
376        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
377        env.options.set(Interactive, On);
378        env.options.set(IgnoreEofOption, On);
379        let ref_env = RefCell::new(&mut env);
380        let mut decorator = IgnoreEof::new(
381            EofStub {
382                inner: Memory::new("echo foo\n"),
383                count: 1,
384            },
385            Fd::STDIN,
386            &ref_env,
387            "EOF ignored\n".to_string(),
388        );
389
390        let result = decorator
391            .next_line(&Context::default())
392            .now_or_never()
393            .unwrap();
394        assert_eq!(result.unwrap(), "");
395        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
396    }
397}