rush-sh 0.8.0

A POSIX sh-compatible shell written in Rust
Documentation
use std::env;
use std::io::Write;
use std::path::Path;

use crate::parser::ShellCommand;
use crate::state::ShellState;

pub struct CdBuiltin;

impl super::Builtin for CdBuiltin {
    fn name(&self) -> &'static str {
        "cd"
    }

    fn names(&self) -> Vec<&'static str> {
        vec![self.name()]
    }

    fn description(&self) -> &'static str {
        "Change directory"
    }

    fn run(
        &self,
        cmd: &ShellCommand,
        shell_state: &mut ShellState,
        output_writer: &mut dyn Write,
    ) -> i32 {
        let dir = if cmd.args.len() > 1 {
            cmd.args[1].clone()
        } else {
            "~".to_string()
        };
        let path = if dir == "~" {
            env::var("HOME").unwrap_or_else(|_| "/".to_string())
        } else {
            dir
        };
        
        // Save current directory as OLDPWD before changing
        if let Ok(current) = env::current_dir() {
            let current_str = current.to_string_lossy().to_string();
            shell_state.set_exported_var("OLDPWD", current_str.clone());
            // Also update the environment variable
            unsafe {
                env::set_var("OLDPWD", current_str);
            }
        }
        
        if let Err(e) = env::set_current_dir(Path::new(&path)) {
            let _ = writeln!(output_writer, "cd: {}: {}", path, e);
            1
        } else {
            // Update PWD to the new directory
            if let Ok(new_dir) = env::current_dir() {
                let new_dir_str = new_dir.to_string_lossy().to_string();
                shell_state.set_exported_var("PWD", new_dir_str.clone());
                // Also update the environment variable so subsequent commands see the new PWD
                unsafe {
                    env::set_var("PWD", new_dir_str);
                }
            }
            0
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::builtins::Builtin;
    use std::env;

    #[test]
    fn test_cd_to_valid_directory() {
        let original_dir = env::current_dir().unwrap();
        let cmd = ShellCommand {
            args: vec!["cd".to_string(), "/tmp".to_string()],
            redirections: Vec::new(),
            compound: None,
        };
        let mut shell_state = ShellState::new();
        let builtin = CdBuiltin;
        let mut output = Vec::new();
        let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
        assert_eq!(exit_code, 0);
        // Restore original directory
        let _ = env::set_current_dir(&original_dir);
    }

    #[test]
    fn test_cd_to_invalid_directory() {
        let cmd = ShellCommand {
            args: vec!["cd".to_string(), "/nonexistent".to_string()],
            redirections: Vec::new(),
            compound: None,
        };
        let mut shell_state = ShellState::new();
        let builtin = CdBuiltin;
        let mut output = Vec::new();
        let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
        assert_eq!(exit_code, 1);
        assert!(output.len() > 0); // Should have error message
    }

    #[test]
    fn test_cd_no_arguments() {
        let cmd = ShellCommand {
            args: vec!["cd".to_string()],
            redirections: Vec::new(),
            compound: None,
        };
        let mut shell_state = ShellState::new();
        let builtin = CdBuiltin;
        let mut output = Vec::new();
        let exit_code = builtin.run(&cmd, &mut shell_state, &mut output);
        assert_eq!(exit_code, 0);
    }
}