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