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