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