yash_builtin/read.rs
1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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//! Read built-in
18//!
19//! This module implements the [`read` built-in], which reads a line into variables.
20//!
21//! [`read` built-in]: https://magicant.github.io/yash-rs/builtins/read.html
22//!
23//! # Implementation notes
24//!
25//! The built-in reads the input byte by byte. This is inefficient, but it is
26//! necessary not to read past the delimiter.
27//! (TODO: Use a buffered reader if the input is seekable)
28//!
29//! Prompting requires a [`GetPrompt`](yash_env::prompt::GetPrompt) instance to
30//! be available in the environment's [`any`](yash_env::Env::any) storage. If no
31//! such instance is found, the built-in will **panic**.
32
33use crate::common::report::{merge_reports, report, report_simple};
34use yash_env::Env;
35use yash_env::semantics::ExitStatus;
36use yash_env::semantics::Field;
37use yash_env::system::{Fcntl, Isatty, Read, Write};
38
39pub mod assigning;
40pub mod input;
41pub mod syntax;
42
43/// Exit status when the built-in succeeds
44pub const EXIT_STATUS_SUCCESS: ExitStatus = ExitStatus(0);
45
46/// Exit status when the built-in reaches the end of the input before finding a newline
47pub const EXIT_STATUS_EOF: ExitStatus = ExitStatus(1);
48
49/// Exit status when the built-in fails to assign a value to a variable
50pub const EXIT_STATUS_ASSIGN_ERROR: ExitStatus = ExitStatus(2);
51
52/// Exit status when the built-in fails to read from the input
53pub const EXIT_STATUS_READ_ERROR: ExitStatus = ExitStatus(3);
54
55/// Exit status on a command line syntax error
56pub const EXIT_STATUS_SYNTAX_ERROR: ExitStatus = ExitStatus(4);
57
58/// Abstract command line arguments of the `read` built-in
59///
60/// An instance of this struct is created by parsing command line arguments
61/// using the [`syntax`] module.
62#[derive(Clone, Debug, Eq, PartialEq)]
63#[non_exhaustive]
64pub struct Command {
65 /// Delimiter specified by the `-d` option
66 ///
67 /// When the option is not specified, this field is `b'\n'`.
68 pub delimiter: u8,
69
70 /// Whether the `-r` option is specified
71 ///
72 /// If this field is `true`, backslashes are not interpreted.
73 pub is_raw: bool,
74
75 /// Names of variables to be assigned, except the last one
76 pub variables: Vec<Field>,
77
78 /// Name of the last variable to be assigned
79 ///
80 /// The last variable receives all remaining fields, including the
81 /// intermediate (but not trailing) field separators.
82 pub last_variable: Field,
83}
84
85/// Entry point of the `read` built-in
86pub async fn main<S>(env: &mut Env<S>, args: Vec<Field>) -> crate::Result
87where
88 S: Fcntl + Isatty + Read + Write + 'static,
89{
90 let command = match syntax::parse(env, args) {
91 Ok(command) => command,
92 Err(error) => return report(env, &error, EXIT_STATUS_SYNTAX_ERROR).await,
93 };
94
95 let (input, newline_found) = match input::read(env, command.delimiter, command.is_raw).await {
96 Ok(input) => input,
97 Err(error) => return report(env, &error, EXIT_STATUS_READ_ERROR).await,
98 };
99
100 if input.iter().any(|c| c.value == '\0') {
101 return report_simple(env, "input contains a nul byte", EXIT_STATUS_READ_ERROR).await;
102 }
103
104 let errors = assigning::assign(env, &input, command.variables, command.last_variable);
105 match merge_reports(&errors) {
106 None if newline_found => EXIT_STATUS_SUCCESS.into(),
107 None => EXIT_STATUS_EOF.into(),
108 Some(report) => self::report(env, report, EXIT_STATUS_ASSIGN_ERROR).await,
109 }
110}