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))
    }
}