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