yash_cli/startup/
input.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//! Preparing input object
18//!
19//! This module implements the [`prepare_input`] function that prepares the input
20//! object for the shell. The input object is constructed from the given source
21//! and decorated with the [`Echo`] and [`Prompter`] decorators as necessary.
22//!
23//! The [`SourceInput`] and [`PrepareInputError`] types define the return value
24//! of the function.
25
26use super::args::Source;
27use std::cell::RefCell;
28use std::ffi::CString;
29use thiserror::Error;
30use yash_env::input::Echo;
31use yash_env::input::FdReader;
32use yash_env::input::IgnoreEof;
33use yash_env::input::Reporter;
34use yash_env::io::Fd;
35use yash_env::option::Option::Interactive;
36use yash_env::option::State::{Off, On};
37use yash_env::system::Errno;
38use yash_env::system::Mode;
39use yash_env::system::OfdAccess;
40use yash_env::system::OpenFlag;
41use yash_env::system::SystemEx as _;
42use yash_env::Env;
43use yash_env::System;
44use yash_prompt::Prompter;
45use yash_syntax::input::InputObject;
46use yash_syntax::input::Memory;
47use yash_syntax::source::Source as SyntaxSource;
48
49/// Result of [`prepare_input`].
50pub struct SourceInput<'a> {
51    /// Input to be passed to the lexer
52    pub input: Box<dyn InputObject + 'a>,
53    /// Description of the source
54    pub source: SyntaxSource,
55}
56
57/// Error returned by [`prepare_input`].
58#[derive(Clone, Debug, Eq, Error, PartialEq)]
59#[error("cannot open script file '{path}': {errno}")]
60pub struct PrepareInputError<'a> {
61    /// Raw error value returned by the underlying system call.
62    pub errno: Errno,
63    /// Path of the script file that could not be opened.
64    pub path: &'a str,
65}
66
67/// Prepares the input for the shell.
68///
69/// This function constructs an input object from the given source with the
70/// following decorators:
71///
72/// - If the source is read with a file descriptor, the [`Echo`] decorator is
73///   applied to the input to implement the [`Verbose`] shell option.
74/// - If the [`Interactive`] option is enabled and the source is read with a
75///   file descriptor, the [`Prompter`] decorator is applied to the input to
76///   show the prompt.
77/// - If the [`Interactive`] option is enabled, the [`Reporter`] decorator is
78///   applied to the input to show changes in job status before prompting for
79///   the next command.
80/// - If the [`Interactive`] option is enabled and the source is read with a
81///   file descriptor, the [`IgnoreEof`] decorator is applied to the input to
82///   implement the [`IgnoreEof`](yash_env::option::IgnoreEof) shell option.
83///
84/// The `RefCell` passed as the first argument should be shared with (and only
85/// with) the [`read_eval_loop`](yash_semantics::read_eval_loop) function that
86/// consumes the input and executes the parsed commands.
87///
88/// [`Verbose`]: yash_env::option::Verbose
89pub fn prepare_input<'s: 'i + 'e, 'i, 'e>(
90    env: &'i RefCell<&mut Env>,
91    source: &'s Source,
92) -> Result<SourceInput<'i>, PrepareInputError<'e>> {
93    match source {
94        Source::Stdin => {
95            let mut system = env.borrow().system.clone();
96            if system.isatty(Fd::STDIN) || system.fd_is_pipe(Fd::STDIN) {
97                // It makes virtually no sense to make it blocking here
98                // since we will be doing non-blocking reads anyway,
99                // but POSIX requires us to do it.
100                // https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/sh.html#tag_20_117_06
101                _ = system.get_and_set_nonblocking(Fd::STDIN, false);
102            }
103
104            let input = prepare_fd_input(Fd::STDIN, env);
105            let source = SyntaxSource::Stdin;
106            Ok(SourceInput { input, source })
107        }
108
109        Source::File { path } => {
110            let mut system = env.borrow().system.clone();
111
112            let c_path = CString::new(path.as_str()).map_err(|_| PrepareInputError {
113                errno: Errno::EILSEQ,
114                path,
115            })?;
116            let fd = system
117                .open(
118                    &c_path,
119                    OfdAccess::ReadOnly,
120                    OpenFlag::CloseOnExec.into(),
121                    Mode::empty(),
122                )
123                .and_then(|fd| system.move_fd_internal(fd))
124                .map_err(|errno| PrepareInputError { errno, path })?;
125
126            let input = prepare_fd_input(fd, env);
127            let path = path.to_owned();
128            let source = SyntaxSource::CommandFile { path };
129            Ok(SourceInput { input, source })
130        }
131
132        Source::String(command) => {
133            let basic_input = Memory::new(command);
134
135            let is_interactive = env.borrow().options.get(Interactive) == On;
136            let input: Box<dyn InputObject> = if is_interactive {
137                Box::new(Reporter::new(basic_input, env))
138            } else {
139                Box::new(basic_input)
140            };
141            let source = SyntaxSource::CommandString;
142            Ok(SourceInput { input, source })
143        }
144    }
145}
146
147/// Creates an input object from a file descriptor.
148///
149/// This function creates an [`FdReader`] object from the given file descriptor
150/// and wraps it with the [`Echo`] decorator. If the [`Interactive`] option is
151/// enabled, the [`Prompter`], [`Reporter`], and [`IgnoreEof`] decorators are
152/// applied to the input object.
153fn prepare_fd_input<'i>(fd: Fd, ref_env: &'i RefCell<&mut Env>) -> Box<dyn InputObject + 'i> {
154    let env = ref_env.borrow();
155    let system = env.system.clone();
156
157    let basic_input = Echo::new(FdReader::new(fd, system), ref_env);
158
159    if env.options.get(Interactive) == Off {
160        Box::new(basic_input)
161    } else {
162        // The order of these decorators is important. The prompt should be shown after
163        // the job status is reported, and both should be shown again if an EOF is ignored.
164        let prompter = Prompter::new(basic_input, ref_env);
165        let reporter = Reporter::new(prompter, ref_env);
166        let message =
167            "# Type `exit` to leave the shell when the ignore-eof option is on.\n".to_string();
168        Box::new(IgnoreEof::new(reporter, fd, ref_env, message))
169    }
170}