jarvish 1.8.3

Next Generation AI Integrated Shell inspired by J.A.R.V.I.S. on Marvel's Iron Man
Documentation
use std::env;
use std::path::PathBuf;

use clap::Parser;

use crate::engine::CommandResult;

/// cd: カレントディレクトリを変更する。
#[derive(Parser)]
#[command(name = "cd", about = "Change the current directory")]
struct CdArgs {
    /// Target path (defaults to $HOME)
    path: Option<String>,
}

/// cd: カレントディレクトリを変更する。
/// - 引数なし → `$HOME` へ移動
/// - 引数あり → 指定パスへ移動
///   展開は execute 側で実施済み
///
/// cd 成功時、変更前のカレントディレクトリを `dir_stack` に push する。
pub(crate) fn execute(args: &[&str], dir_stack: &mut Vec<PathBuf>) -> CommandResult {
    let parsed = match super::parse_args::<CdArgs>("cd", args) {
        Ok(a) => a,
        Err(result) => return result,
    };

    let target: PathBuf = if let Some(path) = parsed.path {
        PathBuf::from(path)
    } else {
        // 引数なしの場合は $HOME        match env::var_os("HOME") {
            Some(home) => PathBuf::from(home),
            None => {
                let msg = "jarvish: cd: HOME not set\n".to_string();
                eprint!("{msg}");
                return CommandResult::error(msg, 1);
            }
        }
    };

    // 変更前の PWD を保存(OLDPWD 用)
    let old_pwd = env::var("PWD").ok().or_else(|| {
        env::current_dir()
            .ok()
            .map(|p| p.to_string_lossy().into_owned())
    });

    match env::set_current_dir(&target) {
        Ok(()) => {
            if let Some(old) = old_pwd {
                dir_stack.push(PathBuf::from(&old));
                env::set_var("OLDPWD", &old);
            }
            if let Ok(new_pwd) = env::current_dir() {
                env::set_var("PWD", &new_pwd);
            }
            CommandResult::success(String::new())
        }
        Err(e) => {
            let msg = format!("jarvish: cd: {}: {e}\n", target.display());
            eprint!("{msg}");
            CommandResult::error(msg, 1)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::builtins::cwd::test_helpers::CwdGuard;
    use crate::engine::LoopAction;
    use serial_test::serial;
    use std::env;
    use std::path::PathBuf;

    #[test]
    #[serial]
    fn cd_to_specified_directory() {
        let _guard = CwdGuard::new();
        let tmpdir = tempfile::tempdir().expect("failed to create tempdir");
        let target = tmpdir.path().to_path_buf();

        let result = execute(&[target.to_str().unwrap()], &mut Vec::new());
        assert_eq!(result.exit_code, 0);
        assert_eq!(result.action, LoopAction::Continue);

        let cwd = env::current_dir().unwrap();
        // macOS では /tmp → /private/tmp にシンボリックリンクされるため canonicalize する
        assert_eq!(cwd.canonicalize().unwrap(), target.canonicalize().unwrap());
    }

    #[test]
    #[serial]
    fn cd_no_args_goes_home() {
        let _guard = CwdGuard::new();
        if let Some(home) = env::var_os("HOME") {
            let result = execute(&[], &mut Vec::new());
            assert_eq!(result.exit_code, 0);

            let cwd = env::current_dir().unwrap();
            assert_eq!(
                cwd.canonicalize().unwrap(),
                PathBuf::from(&home).canonicalize().unwrap()
            );
        }
    }

    #[test]
    #[serial]
    fn cd_nonexistent_path_returns_error() {
        let _guard = CwdGuard::new();
        let result = execute(&["/nonexistent_path_that_does_not_exist"], &mut Vec::new());
        assert_ne!(result.exit_code, 0);
        assert!(result.stderr.contains("cd:"));
    }

    #[test]
    fn cd_help_returns_success() {
        let result = execute(&["--help"], &mut Vec::new());
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.contains("cd"));
    }

    #[test]
    #[serial]
    fn cd_updates_pwd_env_var() {
        let _guard = CwdGuard::new();
        let tmpdir = tempfile::tempdir().expect("failed to create tempdir");
        let target = tmpdir.path().to_path_buf();

        let result = execute(&[target.to_str().unwrap()], &mut Vec::new());
        assert_eq!(result.exit_code, 0);

        // $PWD が新しいディレクトリに更新されていること
        let pwd = env::var("PWD").expect("PWD should be set after cd");
        assert_eq!(
            PathBuf::from(&pwd).canonicalize().unwrap(),
            target.canonicalize().unwrap()
        );
    }

    #[test]
    #[serial]
    fn cd_updates_oldpwd_env_var() {
        let _guard = CwdGuard::new();
        let original_pwd = env::current_dir().unwrap();
        // PWD を現在のディレクトリに明示設定
        env::set_var("PWD", &original_pwd);

        let tmpdir = tempfile::tempdir().expect("failed to create tempdir");
        let target = tmpdir.path().to_path_buf();

        let result = execute(&[target.to_str().unwrap()], &mut Vec::new());
        assert_eq!(result.exit_code, 0);

        // $OLDPWD が変更前のディレクトリを保持していること
        let oldpwd = env::var("OLDPWD").expect("OLDPWD should be set after cd");
        assert_eq!(
            PathBuf::from(&oldpwd).canonicalize().unwrap(),
            original_pwd.canonicalize().unwrap()
        );
    }
}