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