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::{
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    async fn open_fd<S>(system: &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
167            .open(
168                &c_path,
169                OfdAccess::ReadOnly,
170                OpenFlag::CloseOnExec.into(),
171                Mode::empty(),
172            )
173            .await?;
174        move_fd_internal(system, fd)
175    }
176
177    let fd = match open_fd(&env.system, path.to_owned()).await {
178        Ok(fd) => fd,
179        Err(errno) => {
180            env.system
181                .print_error(&format!(
182                    "{}: cannot open initialization file {path:?}: {errno}\n",
183                    &env.arg0
184                ))
185                .await;
186            return;
187        }
188    };
189
190    let env = &mut *env.push_frame(Frame::InitFile);
191    let system = env.system.clone();
192    let ref_env = RefCell::new(&mut *env);
193    let input = Box::new(Echo::new(FdReader2::new(fd, system), &ref_env));
194    let mut config = Config::with_input(input);
195    config.source = Some(Rc::new(Source::InitFile {
196        path: path.to_owned(),
197    }));
198    let mut lexer = config.into();
199    _ = read_eval_loop(&ref_env, &mut { lexer }).await;
200
201    if let Err(errno) = env.system.close(fd) {
202        env.system
203            .print_error(&format!(
204                "{}: cannot close initialization file {path:?}: {errno}\n",
205                &env.arg0
206            ))
207            .await;
208    }
209}
210
211/// Runs the rcfile specified by the `file` argument.
212///
213/// This function resolves the path to the rcfile using [`resolve_rcfile_path`]
214/// and then runs the rcfile using [`run_init_file`]. Any errors resolving the
215/// path are reported to the standard error.
216pub async fn run_rcfile<S>(env: &mut Env<S>, file: InitFile)
217where
218    S: GetUid + Runtime + 'static,
219{
220    match resolve_rcfile_path(env, file).await {
221        Ok(path) => run_init_file(env, &path).await,
222        Err(e) => drop(e.handle(env).await),
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use assert_matches::assert_matches;
230    use futures_util::FutureExt as _;
231    use yash_env::VirtualSystem;
232    use yash_env::option::State::On;
233    use yash_env::system::{Gid, Uid};
234    use yash_env::variable::Scope::Global;
235
236    #[test]
237    fn default_rcfile_path_with_unset_env() {
238        let mut env = Env::new_virtual();
239        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
240        assert_eq!(result.unwrap(), "");
241    }
242
243    #[test]
244    fn default_rcfile_path_with_empty_env() {
245        let mut env = Env::new_virtual();
246        env.variables
247            .get_or_new(ENV, Global)
248            .assign("", None)
249            .unwrap();
250        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
251        assert_eq!(result.unwrap(), "");
252    }
253
254    #[test]
255    fn default_rcfile_path_with_env_without_expansion() {
256        let mut env = Env::new_virtual();
257        env.variables
258            .get_or_new(ENV, Global)
259            .assign("foo", None)
260            .unwrap();
261        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
262        assert_eq!(result.unwrap(), "foo");
263    }
264
265    #[test]
266    fn default_rcfile_path_with_env_with_unparsable_expansion() {
267        let mut env = Env::new_virtual();
268        env.variables
269            .get_or_new(ENV, Global)
270            .assign("foo${bar", None)
271            .unwrap();
272        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
273        assert_matches!(result, Err(DefaultFilePathError::ParseError(_)));
274    }
275
276    #[test]
277    fn default_rcfile_path_with_env_with_failing_expansion() {
278        let mut env = Env::new_virtual();
279        env.variables
280            .get_or_new(ENV, Global)
281            .assign("${unset?}", None)
282            .unwrap();
283        let result = default_rcfile_path(&mut env).now_or_never().unwrap();
284        assert_matches!(result, Err(DefaultFilePathError::ExpansionError(_)));
285    }
286
287    #[test]
288    fn resolve_rcfile_path_none() {
289        let mut env = Env::new_virtual();
290        env.options.set(Interactive, On);
291        let result = resolve_rcfile_path(&mut env, InitFile::None)
292            .now_or_never()
293            .unwrap();
294        assert_eq!(result.unwrap(), "");
295    }
296
297    #[test]
298    fn resolve_rcfile_path_default() {
299        let mut env = Env::new_virtual();
300        env.options.set(Interactive, On);
301        env.variables
302            .get_or_new(ENV, Global)
303            .assign("foo/bar", None)
304            .unwrap();
305        let result = resolve_rcfile_path(&mut env, InitFile::Default)
306            .now_or_never()
307            .unwrap();
308        assert_eq!(result.unwrap(), "foo/bar");
309    }
310
311    #[test]
312    fn resolve_rcfile_path_exact() {
313        let mut env = Env::new_virtual();
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(), "/path/to/rcfile");
319    }
320
321    #[test]
322    fn resolve_rcfile_path_non_interactive() {
323        let mut env = Env::new_virtual();
324        env.options.set(Interactive, Off);
325        let path = "/path/to/rcfile".to_string();
326        let file = InitFile::File { path };
327        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
328        assert_eq!(result.unwrap(), "");
329    }
330
331    #[test]
332    fn resolve_rcfile_path_non_real_user() {
333        let system = VirtualSystem::new();
334        system.current_process_mut().set_uid(Uid(0));
335        system.current_process_mut().set_euid(Uid(10));
336        let mut env = Env::with_system(system);
337        env.options.set(Interactive, On);
338        let path = "/path/to/rcfile".to_string();
339        let file = InitFile::File { path };
340        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
341        assert_eq!(result.unwrap(), "");
342    }
343
344    #[test]
345    fn resolve_rcfile_path_non_real_group() {
346        let system = VirtualSystem::new();
347        system.current_process_mut().set_gid(Gid(0));
348        system.current_process_mut().set_egid(Gid(10));
349        let mut env = Env::with_system(system);
350        env.options.set(Interactive, On);
351        let path = "/path/to/rcfile".to_string();
352        let file = InitFile::File { path };
353        let result = resolve_rcfile_path(&mut env, file).now_or_never().unwrap();
354        assert_eq!(result.unwrap(), "");
355    }
356}