Skip to main content

yash_builtin/
cd.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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//! Cd built-in
18//!
19//! This module implements the [`cd` built-in], which changes the working directory.
20//!
21//! [`cd` built-in]: https://magicant.github.io/yash-rs/builtins/cd.html
22
23use crate::Result;
24use crate::common::report::report;
25use yash_env::Env;
26use yash_env::path::Path;
27use yash_env::path::PathBuf;
28use yash_env::semantics::ExitStatus;
29use yash_env::semantics::Field;
30use yash_env::source::pretty::{Footnote, FootnoteType, Report, ReportType};
31use yash_env::system::{Chdir, Errno, Fcntl, Fstat, GetCwd, Isatty, Write};
32use yash_env::variable::PWD;
33
34/// Exit status when the built-in succeeds
35pub const EXIT_STATUS_SUCCESS: ExitStatus = ExitStatus(0);
36
37/// Exit status when the new `$PWD` value cannot be determined
38pub const EXIT_STATUS_STALE_PWD: ExitStatus = ExitStatus(1);
39
40/// Exit status when `$PWD` or `$OLDPWD` cannot be updated
41pub const EXIT_STATUS_ASSIGN_ERROR: ExitStatus = ExitStatus(1);
42
43/// Exit status for an error in the underlying `chdir` system call
44pub const EXIT_STATUS_CHDIR_ERROR: ExitStatus = ExitStatus(2);
45
46/// Exit status for when canonicalization fails because of a `..` component
47/// referring to a non-existent directory
48pub const EXIT_STATUS_CANNOT_CANONICALIZE: ExitStatus = ExitStatus(3);
49
50/// Exit status for an unset or empty `$HOME` or `$OLDPWD`
51pub const EXIT_STATUS_UNSET_VARIABLE: ExitStatus = ExitStatus(4);
52
53/// Exit status for invalid command arguments
54pub const EXIT_STATUS_SYNTAX_ERROR: ExitStatus = ExitStatus(5);
55
56/// Treatments of symbolic links in the pathname
57#[derive(Debug, Clone, Copy, Default, Eq, Hash, PartialEq)]
58#[non_exhaustive]
59pub enum Mode {
60    /// Treat the pathname literally without resolving symbolic links
61    #[default]
62    Logical,
63
64    /// Resolve symbolic links in the pathname
65    Physical,
66}
67
68/// Parsed command line arguments
69#[derive(Debug, Clone, Default, Eq, PartialEq)]
70#[non_exhaustive]
71pub struct Command {
72    /// Treatments of symbolic links in the pathname
73    ///
74    /// The `-L` and `-P` options are translated to this field.
75    pub mode: Mode,
76
77    /// Whether to ensure the new `$PWD` value
78    ///
79    /// The `-e` option is translated to this field.
80    /// This option must be used together with the `-P` option.
81    pub ensure_pwd: bool,
82
83    /// The operand that specifies the directory to change to
84    pub operand: Option<Field>,
85}
86
87pub mod assign;
88pub mod canonicalize;
89pub mod cdpath;
90pub mod chdir;
91pub mod print;
92pub mod shorten;
93pub mod syntax;
94pub mod target;
95
96fn get_pwd<S>(env: &Env<S>) -> String {
97    env.variables.get_scalar(PWD).unwrap_or_default().to_owned()
98}
99
100/// Reports that the new `$PWD` value cannot be determined, and returns the
101/// corresponding exit status.
102async fn report_pwd_error<S>(env: &mut Env<S>, errno: Errno, ensure_pwd: bool) -> Result
103where
104    S: Fcntl + Isatty + Write,
105{
106    let (r#type, exit_status) = if ensure_pwd {
107        (ReportType::Error, EXIT_STATUS_STALE_PWD)
108    } else {
109        (ReportType::Warning, EXIT_STATUS_SUCCESS)
110    };
111
112    let mut report = Report::new();
113    report.r#type = r#type;
114    report.title = "cannot compute new $PWD".into();
115    report.footnotes.push(Footnote {
116        r#type: FootnoteType::Note,
117        label: format!("error from underlying system call: {errno}").into(),
118    });
119    self::report(env, report, exit_status).await
120}
121
122/// Entry point for executing the `cd` built-in
123///
124/// This function uses functions in the submodules to execute the built-in.
125pub async fn main<S>(env: &mut Env<S>, args: Vec<Field>) -> Result
126where
127    S: Chdir + Fcntl + Fstat + GetCwd + Isatty + Write,
128{
129    let command = match syntax::parse(env, args) {
130        Ok(command) => command,
131        Err(e) => return report(env, &e, EXIT_STATUS_SYNTAX_ERROR).await,
132    };
133
134    let pwd = get_pwd(env);
135
136    let (path, origin) = match target::target(env, &command, &pwd) {
137        Ok(target) => target,
138        Err(e) => return report(env, &e, e.exit_status()).await,
139    };
140
141    let short_path = shorten::shorten(&path, Path::new(&pwd), command.mode);
142
143    match chdir::chdir(env, short_path) {
144        Ok(()) => {}
145        Err(e) => return chdir::report_failure(env, command.operand.as_ref(), &path, &e).await,
146    }
147
148    let (new_pwd, result1) = match assign::new_pwd(env, command.mode, &path) {
149        Ok(new_pwd) => (new_pwd, Result::from(EXIT_STATUS_SUCCESS)),
150        Err(errno) => (
151            PathBuf::default(),
152            report_pwd_error(env, errno, command.ensure_pwd).await,
153        ),
154    };
155
156    print::print_path(env, &new_pwd, &origin).await;
157
158    let result2 = assign::set_oldpwd(env, pwd).await;
159    let result3 = assign::set_pwd(env, new_pwd).await;
160
161    result1.max(result2).max(result3)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use futures_util::FutureExt as _;
168    use std::rc::Rc;
169    use yash_env::VirtualSystem;
170    use yash_env::test_helper::assert_stderr;
171
172    #[test]
173    fn report_pwd_error_with_ensure_pwd() {
174        let system = VirtualSystem::new();
175        let state = Rc::clone(&system.state);
176        let mut env = Env::with_system(system);
177
178        let result = report_pwd_error(&mut env, Errno::ENAMETOOLONG, true)
179            .now_or_never()
180            .unwrap();
181
182        // Something should be printed
183        assert_stderr(&state, |stderr| assert!(!stderr.is_empty()));
184
185        assert_eq!(result, Result::from(ExitStatus(1)));
186    }
187
188    #[test]
189    fn report_pwd_error_without_ensure_pwd() {
190        let system = VirtualSystem::new();
191        let state = Rc::clone(&system.state);
192        let mut env = Env::with_system(system);
193
194        let result = report_pwd_error(&mut env, Errno::ENAMETOOLONG, false)
195            .now_or_never()
196            .unwrap();
197
198        // Something should be printed
199        assert_stderr(&state, |stderr| assert!(!stderr.is_empty()));
200
201        assert_eq!(result, Result::from(ExitStatus(0)));
202    }
203}