Files
shed/src/builtin/cd.rs
pagedmov 307386ffc6 tightened up some logic with indenting and joining lines
added more linebuf tests

extracted all verb match arms into private methods on LineBuf
2026-03-13 19:24:30 -04:00

243 lines
6.1 KiB
Rust

use ariadne::Fmt;
use yansi::Color;
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node, execute::prepare_argv},
prelude::*,
state::{self},
};
pub fn cd(node: Node) -> ShResult<()> {
let span = node.get_span();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let cd_span = argv.first().unwrap().span.clone();
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() {
argv.remove(0);
}
let (new_dir, arg_span) = if let Some((arg, span)) = argv.into_iter().next() {
(PathBuf::from(arg), Some(span))
} else {
(PathBuf::from(env::var("HOME").unwrap()), None)
};
if !new_dir.exists() {
let mut err = ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), "Failed to change directory");
if let Some(span) = arg_span {
err = err.labeled(
span,
format!(
"No such file or directory '{}'",
new_dir.display().fg(next_color())
),
);
}
return Err(err);
}
if !new_dir.is_dir() {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
cd_span.clone(),
format!(
"cd: Not a directory '{}'",
new_dir.display().fg(next_color())
),
));
}
if let Err(e) = state::change_dir(new_dir) {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
cd_span.clone(),
format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)),
));
}
let new_dir = env::current_dir().map_err(|e| {
ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
cd_span.clone(),
format!(
"cd: Failed to get current directory: '{}'",
e.fg(Color::Red)
),
)
})?;
unsafe { env::set_var("PWD", new_dir) };
state::set_status(0);
Ok(())
}
#[cfg(test)]
pub mod tests {
use std::env;
use std::fs;
use tempfile::TempDir;
use crate::state;
use crate::testutil::{TestGuard, test_input};
// ===================== Basic Navigation =====================
#[test]
fn cd_simple() {
let _g = TestGuard::new();
let old_dir = env::current_dir().unwrap();
let temp_dir = TempDir::new().unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let new_dir = env::current_dir().unwrap();
assert_ne!(old_dir, new_dir);
assert_eq!(
new_dir.display().to_string(),
temp_dir.path().display().to_string()
);
}
#[test]
fn cd_no_args_goes_home() {
let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
unsafe { env::set_var("HOME", temp_dir.path()) };
test_input("cd").unwrap();
let cwd = env::current_dir().unwrap();
assert_eq!(
cwd.display().to_string(),
temp_dir.path().display().to_string()
);
}
#[test]
fn cd_relative_path() {
let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
let sub = temp_dir.path().join("child");
fs::create_dir(&sub).unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
test_input("cd child").unwrap();
let cwd = env::current_dir().unwrap();
assert_eq!(cwd.display().to_string(), sub.display().to_string());
}
// ===================== Environment =====================
#[test]
fn cd_sets_pwd_env() {
let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let pwd = env::var("PWD").unwrap();
assert_eq!(pwd, env::current_dir().unwrap().display().to_string());
}
#[test]
fn cd_status_zero_on_success() {
let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
assert_eq!(state::get_status(), 0);
}
// ===================== Error Cases =====================
#[test]
fn cd_nonexistent_dir_fails() {
let _g = TestGuard::new();
let result = test_input("cd /nonexistent_path_that_does_not_exist_xyz");
assert!(result.is_err());
}
#[test]
fn cd_file_not_directory_fails() {
let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("afile.txt");
fs::write(&file_path, "hello").unwrap();
let result = test_input(format!("cd {}", file_path.display()));
assert!(result.is_err());
}
// ===================== Multiple cd =====================
#[test]
fn cd_multiple_times() {
let _g = TestGuard::new();
let dir_a = TempDir::new().unwrap();
let dir_b = TempDir::new().unwrap();
test_input(format!("cd {}", dir_a.path().display())).unwrap();
assert_eq!(
env::current_dir().unwrap().display().to_string(),
dir_a.path().display().to_string()
);
test_input(format!("cd {}", dir_b.path().display())).unwrap();
assert_eq!(
env::current_dir().unwrap().display().to_string(),
dir_b.path().display().to_string()
);
}
#[test]
fn cd_nested_subdirectories() {
let _g = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
let deep = temp_dir.path().join("a").join("b").join("c");
fs::create_dir_all(&deep).unwrap();
test_input(format!("cd {}", deep.display())).unwrap();
assert_eq!(
env::current_dir().unwrap().display().to_string(),
deep.display().to_string()
);
}
// ===================== Autocmd Integration =====================
#[test]
fn cd_fires_post_change_dir_autocmd() {
let guard = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
test_input("autocmd post-change-dir 'echo cd-hook-fired'").unwrap();
guard.read_output();
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let out = guard.read_output();
assert!(out.contains("cd-hook-fired"));
}
#[test]
fn cd_fires_pre_change_dir_autocmd() {
let guard = TestGuard::new();
let temp_dir = TempDir::new().unwrap();
test_input("autocmd pre-change-dir 'echo pre-cd'").unwrap();
guard.read_output();
test_input(format!("cd {}", temp_dir.path().display())).unwrap();
let out = guard.read_output();
assert!(out.contains("pre-cd"));
}
}