yash_cli/startup/
init_file.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//! Running initialization files
18//!
19//! This module provides functions for running initialization files in the shell.
20//! The initialization file is a script that is executed when the shell starts up.
21//!
22//! Currently, this module only supports running the POSIX-defined rcfile, whose
23//! path is determined by the value of the `ENV` environment variable.
24//! (TODO: Support for yash-specific initialization files will be added later.)
25//!
26//! The [`run_rcfile`] function is the main entry point for running the rcfile.
27//! Helper functions that are used by `run_rcfile` are also provided in this
28//! module.
29
30use super::args::InitFile;
31use std::cell::RefCell;
32use std::ffi::CString;
33use std::rc::Rc;
34use thiserror::Error;
35use yash_env::Env;
36use yash_env::System;
37use yash_env::input::{Echo, FdReader};
38use yash_env::io::Fd;
39use yash_env::option::Option::Interactive;
40use yash_env::option::State::Off;
41use yash_env::stack::Frame;
42use yash_env::system::{Errno, Mode, OfdAccess, OpenFlag, SystemEx};
43use yash_env::variable::ENV;
44use yash_semantics::Handle;
45use yash_semantics::expansion::expand_text;
46use yash_semantics::read_eval_loop;
47use yash_syntax::parser::lex::Lexer;
48use yash_syntax::source::Source;
49
50/// Errors that can occur when finding the default initialization file path
51#[derive(Clone, Debug, Error, PartialEq)]
52#[error(transparent)]
53pub enum DefaultFilePathError {
54    /// An error occurred while parsing the value of a variable specifying the
55    /// initialization file path
56    ParseError(#[from] yash_syntax::parser::Error),
57    /// An error occurred while expanding the value of a variable specifying
58    /// the initialization file path
59    ExpansionError(#[from] yash_semantics::expansion::Error),
60}
61
62impl Handle for DefaultFilePathError {
63    async fn handle(&self, env: &mut Env) -> yash_semantics::Result {
64        match self {
65            DefaultFilePathError::ParseError(e) => e.handle(env).await,
66            DefaultFilePathError::ExpansionError(e) => e.handle(env).await,
67        }
68    }
69}
70
71/// Finds the path to the default rcfile.
72///
73/// The default path is determined by the value of the [`ENV`] environment
74/// variable. The value is parsed as a [`Text`] and subjected to the
75/// [initial expansion].
76///
77/// If the variable does not exist or is empty, the result will be an empty
78/// string.
79///
80/// If the variable value cannot be parsed or expanded, an error message will
81/// be printed to the standard error and an empty string will be returned.
82///
83/// TODO: If the POSIXly correct mode is off, the default path should be
84/// `~/.yashrc` (or maybe some XDG-compliant path).
85///
86/// [`ENV`]: yash_env::variable::ENV
87/// [`Text`]: yash_syntax::syntax::Text
88/// [initial expansion]: yash_semantics::expansion::initial
89pub async fn default_rcfile_path(env: &mut Env) -> Result<String, DefaultFilePathError> {
90    let raw_value = env.variables.get_scalar(ENV).unwrap_or_default();
91
92    let text = {
93        let name = ENV.to_owned();
94        let source = Source::VariableValue { name };
95        let mut lexer = Lexer::from_memory(raw_value, source);
96        lexer.text(|_| false, |_| false).await?
97    };
98
99    Ok(expand_text(env, &text).await?.0)
100}
101
102/// Resolves the path to the rcfile.
103///
104/// This function resolves the path to the rcfile specified by the `file`
105/// argument. If the file is `InitFile::Default`, the default rcfile path is
106/// determined by calling [`default_rcfile_path`].
107///
108/// This function returns an empty string in the following cases, in which
109/// case the rcfile should not be executed:
110///
111/// - `file` is `InitFile::None`,
112/// - the `Interactive` shell option is off,
113/// - the real user ID of the process is not the same as the effective user ID, or
114/// - the real group ID of the process is not the same as the effective group ID.
115pub async fn resolve_rcfile_path(
116    env: &mut Env,
117    file: InitFile,
118) -> Result<String, DefaultFilePathError> {
119    if file == InitFile::None
120        || env.options.get(Interactive) == Off
121        || env.system.getuid() != env.system.geteuid()
122        || env.system.getgid() != env.system.getegid()
123    {
124        return Ok(String::default());
125    }
126
127    match file {
128        InitFile::None => unreachable!(),
129        InitFile::Default => default_rcfile_path(env).await,
130        InitFile::File { path } => Ok(path),
131    }
132}
133
134/// Runs an initialization file, reading from the specified path.
135///
136/// This function reads the contents of the initialization file and executes
137/// them in the current shell environment. The file is specified by the `path`
138/// argument.
139///
140/// If `path` is an empty string, the function returns immediately.
141pub async fn run_init_file(env: &mut Env, path: &str) {
142    if path.is_empty() {
143        return;
144    }
145
146    fn open_fd<S: System>(system: &mut S, path: String) -> Result<Fd, Errno> {
147        let c_path = CString::new(path).map_err(|_| Errno::EILSEQ)?;
148        let fd = system.open(
149            &c_path,
150            OfdAccess::ReadOnly,
151            OpenFlag::CloseOnExec.into(),
152            Mode::empty(),
153        )?;
154        system.move_fd_internal(fd)
155    }
156
157    let fd = match open_fd(&mut env.system, path.to_owned()) {
158        Ok(fd) => fd,
159        Err(errno) => {
160            env.system
161                .print_error(&format!(
162                    "{}: cannot open initialization file {path:?}: {errno}\n",
163                    &env.arg0
164                ))
165                .await;
166            return;
167        }
168    };
169
170    let env = &mut *env.push_frame(Frame::InitFile);
171    let system = env.system.clone();
172    let ref_env = RefCell::new(&mut *env);
173    let mut config = Lexer::config();
174    config.source = Some(Rc::new(Source::InitFile {
175        path: path.to_owned(),
176    }));
177    let input = Box::new(Echo::new(FdReader::new(fd, system), &ref_env));
178    let mut lexer = config.input(input);
179    _ = read_eval_loop(&ref_env, &mut { lexer }).await;
180
181    if let Err(errno) = env.system.close(fd) {
182        env.system
183            .print_error(&format!(
184                "{}: cannot close initialization file {path:?}: {errno}\n",
185                &env.arg0
186            ))
187            .await;
188    }
189}
190
191/// Runs the rcfile specified by the `file` argument.
192///
193/// This function resolves the path to the rcfile using [`resolve_rcfile_path`]
194/// and then runs the rcfile using [`run_init_file`]. Any errors resolving the
195/// path are reported to the standard error.
196pub async fn run_rcfile(env: &mut Env, file: InitFile) {
197    match resolve_rcfile_path(env, file).await {
198        Ok(path) => run_init_file(env, &path).await,
199        Err(e) => drop(e.handle(env).await),
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use assert_matches::assert_matches;
207    use futures_util::FutureExt as _;
208    use yash_env::VirtualSystem;
209    use yash_env::option::State::On;
210    use yash_env::system::{Gid, Uid};
211    use yash_env::variable::Scope::Global;
212
213    #[test]
214    fn default_rcfile_path_with_unset_env() {
215        let mut env = Env::new_virtual();
216        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
217        assert_eq!(result.unwrap(), "");
218    }
219
220    #[test]
221    fn default_rcfile_path_with_empty_env() {
222        let mut env = Env::new_virtual();
223        env.variables
224            .get_or_new(ENV, Global)
225            .assign("", None)
226            .unwrap();
227        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
228        assert_eq!(result.unwrap(), "");
229    }
230
231    #[test]
232    fn default_rcfile_path_with_env_without_expansion() {
233        let mut env = Env::new_virtual();
234        env.variables
235            .get_or_new(ENV, Global)
236            .assign("foo", None)
237            .unwrap();
238        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
239        assert_eq!(result.unwrap(), "foo");
240    }
241
242    #[test]
243    fn default_rcfile_path_with_env_with_unparsable_expansion() {
244        let mut env = Env::new_virtual();
245        env.variables
246            .get_or_new(ENV, Global)
247            .assign("foo${bar", None)
248            .unwrap();
249        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
250        assert_matches!(result, Err(DefaultFilePathError::ParseError(_)));
251    }
252
253    #[test]
254    fn default_rcfile_path_with_env_with_failing_expansion() {
255        let mut env = Env::new_virtual();
256        env.variables
257            .get_or_new(ENV, Global)
258            .assign("${unset?}", None)
259            .unwrap();
260        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
261        assert_matches!(result, Err(DefaultFilePathError::ExpansionError(_)));
262    }
263
264    #[test]
265    fn resolve_rcfile_path_none() {
266        let mut env = Env::new_virtual();
267        env.options.set(Interactive, On);
268        let result = resolve_rcfile_path(&mut env, InitFile::None)
269            .now_or_never()
270            .unwrap();
271        assert_eq!(result.unwrap(), "");
272    }
273
274    #[test]
275    fn resolve_rcfile_path_default() {
276        let mut env = Env::new_virtual();
277        env.options.set(Interactive, On);
278        env.variables
279            .get_or_new(ENV, Global)
280            .assign("foo/bar", None)
281            .unwrap();
282        let result = resolve_rcfile_path(&mut env, InitFile::Default)
283            .now_or_never()
284            .unwrap();
285        assert_eq!(result.unwrap(), "foo/bar");
286    }
287
288    #[test]
289    fn resolve_rcfile_path_exact() {
290        let mut env = Env::new_virtual();
291        env.options.set(Interactive, On);
292        let path = "/path/to/rcfile".to_string();
293        let file = InitFile::File { path };
294        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
295        assert_eq!(result.unwrap(), "/path/to/rcfile");
296    }
297
298    #[test]
299    fn resolve_rcfile_path_non_interactive() {
300        let mut env = Env::new_virtual();
301        env.options.set(Interactive, Off);
302        let path = "/path/to/rcfile".to_string();
303        let file = InitFile::File { path };
304        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
305        assert_eq!(result.unwrap(), "");
306    }
307
308    #[test]
309    fn resolve_rcfile_path_non_real_user() {
310        let mut system = Box::new(VirtualSystem::new());
311        system.current_process_mut().set_uid(Uid(0));
312        system.current_process_mut().set_euid(Uid(10));
313        let mut env = Env::with_system(system);
314        env.options.set(Interactive, On);
315        let path = "/path/to/rcfile".to_string();
316        let file = InitFile::File { path };
317        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
318        assert_eq!(result.unwrap(), "");
319    }
320
321    #[test]
322    fn resolve_rcfile_path_non_real_group() {
323        let mut system = Box::new(VirtualSystem::new());
324        system.current_process_mut().set_gid(Gid(0));
325        system.current_process_mut().set_egid(Gid(10));
326        let mut env = Env::with_system(system);
327        env.options.set(Interactive, On);
328        let path = "/path/to/rcfile".to_string();
329        let file = InitFile::File { path };
330        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
331        assert_eq!(result.unwrap(), "");
332    }
333}