Skip to main content

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