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::tests::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 {
158                        file: 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                    })),
167                    flags: EnumSet::empty(),
168                },
169            )
170            .unwrap();
171    }
172
173    fn set_stdin_to_regular_file(system: &mut VirtualSystem) {
174        system
175            .current_process_mut()
176            .set_fd(
177                Fd::STDIN,
178                FdBody {
179                    open_file_description: Rc::new(RefCell::new(OpenFileDescription {
180                        file: Rc::new(RefCell::new(Inode {
181                            body: FileBody::Regular {
182                                content: vec![],
183                                is_native_executable: false,
184                            },
185                            permissions: Mode::empty(),
186                        })),
187                        offset: 0,
188                        is_readable: true,
189                        is_writable: true,
190                        is_appending: false,
191                    })),
192                    flags: EnumSet::empty(),
193                },
194            )
195            .unwrap();
196    }
197
198    #[test]
199    fn decorator_reads_from_inner_input() {
200        let mut system = VirtualSystem::new();
201        set_stdin_to_tty(&mut system);
202        let mut env = Env::with_system(system);
203        env.options.set(Interactive, On);
204        env.options.set(IgnoreEofOption, On);
205        let ref_env = RefCell::new(&mut env);
206        let mut decorator = IgnoreEof::new(
207            Memory::new("echo foo\n"),
208            Fd::STDIN,
209            &ref_env,
210            "unused".to_string(),
211        );
212
213        let result = decorator
214            .next_line(&Context::default())
215            .now_or_never()
216            .unwrap();
217        assert_eq!(result.unwrap(), "echo foo\n");
218    }
219
220    #[test]
221    fn decorator_reads_input_again_on_eof() {
222        let mut system = VirtualSystem::new();
223        set_stdin_to_tty(&mut system);
224        let state = system.state.clone();
225        let mut env = Env::with_system(system);
226        env.options.set(Interactive, On);
227        env.options.set(IgnoreEofOption, On);
228        let ref_env = RefCell::new(&mut env);
229        let mut decorator = IgnoreEof::new(
230            EofStub {
231                inner: Memory::new("echo foo\n"),
232                count: 1,
233            },
234            Fd::STDIN,
235            &ref_env,
236            "EOF ignored\n".to_string(),
237        );
238
239        let result = decorator
240            .next_line(&Context::default())
241            .now_or_never()
242            .unwrap();
243        assert_eq!(result.unwrap(), "echo foo\n");
244        assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
245    }
246
247    #[test]
248    fn decorator_reads_input_up_to_50_times() {
249        let mut system = VirtualSystem::new();
250        set_stdin_to_tty(&mut system);
251        let state = system.state.clone();
252        let mut env = Env::with_system(system);
253        env.options.set(Interactive, On);
254        env.options.set(IgnoreEofOption, On);
255        let ref_env = RefCell::new(&mut env);
256        let mut decorator = IgnoreEof::new(
257            EofStub {
258                inner: Memory::new("echo foo\n"),
259                count: 50,
260            },
261            Fd::STDIN,
262            &ref_env,
263            "EOF ignored\n".to_string(),
264        );
265
266        let result = decorator
267            .next_line(&Context::default())
268            .now_or_never()
269            .unwrap();
270        assert_eq!(result.unwrap(), "echo foo\n");
271        assert_stderr(&state, |stderr| {
272            assert_eq!(stderr, "EOF ignored\n".repeat(50))
273        });
274    }
275
276    #[test]
277    fn decorator_returns_empty_line_after_reading_51_times() {
278        let mut system = VirtualSystem::new();
279        set_stdin_to_tty(&mut system);
280        let state = system.state.clone();
281        let mut env = Env::with_system(system);
282        env.options.set(Interactive, On);
283        env.options.set(IgnoreEofOption, On);
284        let ref_env = RefCell::new(&mut env);
285        let mut decorator = IgnoreEof::new(
286            EofStub {
287                inner: Memory::new("echo foo\n"),
288                count: 51,
289            },
290            Fd::STDIN,
291            &ref_env,
292            "EOF ignored\n".to_string(),
293        );
294
295        let result = decorator
296            .next_line(&Context::default())
297            .now_or_never()
298            .unwrap();
299        assert_eq!(result.unwrap(), "");
300        assert_stderr(&state, |stderr| {
301            assert_eq!(stderr, "EOF ignored\n".repeat(50))
302        });
303    }
304
305    #[test]
306    fn decorator_returns_immediately_if_not_interactive() {
307        let mut system = VirtualSystem::new();
308        set_stdin_to_tty(&mut system);
309        let state = system.state.clone();
310        let mut env = Env::with_system(system);
311        env.options.set(Interactive, Off);
312        env.options.set(IgnoreEofOption, On);
313        let ref_env = RefCell::new(&mut env);
314        let mut decorator = IgnoreEof::new(
315            EofStub {
316                inner: Memory::new("echo foo\n"),
317                count: 1,
318            },
319            Fd::STDIN,
320            &ref_env,
321            "EOF ignored\n".to_string(),
322        );
323
324        let result = decorator
325            .next_line(&Context::default())
326            .now_or_never()
327            .unwrap();
328        assert_eq!(result.unwrap(), "");
329        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
330    }
331
332    #[test]
333    fn decorator_returns_immediately_if_not_ignore_eof() {
334        let mut system = VirtualSystem::new();
335        set_stdin_to_tty(&mut system);
336        let state = system.state.clone();
337        let mut env = Env::with_system(system);
338        env.options.set(Interactive, On);
339        env.options.set(IgnoreEofOption, Off);
340        let ref_env = RefCell::new(&mut env);
341        let mut decorator = IgnoreEof::new(
342            EofStub {
343                inner: Memory::new("echo foo\n"),
344                count: 1,
345            },
346            Fd::STDIN,
347            &ref_env,
348            "EOF ignored\n".to_string(),
349        );
350
351        let result = decorator
352            .next_line(&Context::default())
353            .now_or_never()
354            .unwrap();
355        assert_eq!(result.unwrap(), "");
356        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
357    }
358
359    #[test]
360    fn decorator_returns_immediately_if_not_terminal() {
361        let mut system = VirtualSystem::new();
362        set_stdin_to_regular_file(&mut system);
363        let state = system.state.clone();
364        let mut env = Env::with_system(system);
365        env.options.set(Interactive, On);
366        env.options.set(IgnoreEofOption, On);
367        let ref_env = RefCell::new(&mut env);
368        let mut decorator = IgnoreEof::new(
369            EofStub {
370                inner: Memory::new("echo foo\n"),
371                count: 1,
372            },
373            Fd::STDIN,
374            &ref_env,
375            "EOF ignored\n".to_string(),
376        );
377
378        let result = decorator
379            .next_line(&Context::default())
380            .now_or_never()
381            .unwrap();
382        assert_eq!(result.unwrap(), "");
383        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
384    }
385}