1use crate::{
2 error::to_shell_err,
3 globals::{get_pwd, get_vfs, set_pwd},
4};
5use nu_engine::CallExt;
6use nu_protocol::{
7 Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, Type,
8 engine::{Command, EngineState, Stack},
9};
10use std::sync::Arc;
11use vfs::VfsFileType;
12
13#[derive(Clone)]
14pub struct Cd;
15
16impl Command for Cd {
17 fn name(&self) -> &str {
18 "cd"
19 }
20
21 fn signature(&self) -> Signature {
22 Signature::build("cd")
23 .optional("path", SyntaxShape::Filepath, "the path to change into")
24 .input_output_type(Type::Nothing, Type::Nothing)
25 .category(Category::FileSystem)
26 }
27
28 fn description(&self) -> &str {
29 "change the current directory in the virtual filesystem."
30 }
31
32 fn run(
33 &self,
34 engine_state: &EngineState,
35 stack: &mut Stack,
36 call: &nu_protocol::engine::Call,
37 _input: PipelineData,
38 ) -> Result<PipelineData, ShellError> {
39 let path_arg: Option<String> = call.opt(engine_state, stack, 0)?;
40 let path = path_arg.unwrap_or_else(|| "/".to_string());
41
42 let base: Arc<vfs::VfsPath> = if path.starts_with('/') {
43 get_vfs()
44 } else {
45 get_pwd().clone()
46 };
47
48 let target = base
49 .join(path.trim_end_matches('/'))
50 .map_err(to_shell_err(call.head))?;
51
52 // Ensure target exists and is a directory
53 let metadata = target.metadata().map_err(to_shell_err(call.head))?;
54 match metadata.file_type {
55 VfsFileType::Directory => {
56 stack.add_env_var(
57 "PWD".to_string(),
58 target.as_str().into_value(call.arguments_span()),
59 );
60 set_pwd(Arc::new(target));
61 Ok(PipelineData::Empty)
62 }
63 VfsFileType::File => Err(ShellError::GenericError {
64 error: "not a directory".to_string(),
65 msg: format!("'{}' is not a directory", path),
66 span: Some(call.head),
67 help: None,
68 inner: vec![],
69 }),
70 }
71 }
72}