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