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