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