add a move command

ptr.pet b7bacebe 5ea7fa86

verified
Changed files
+150 -2
src
+2
src/cmd/mod.rs
··· 5 5 pub mod job_list; 6 6 pub mod ls; 7 7 pub mod mkdir; 8 + pub mod mv; 8 9 pub mod open; 9 10 pub mod pwd; 10 11 pub mod random; ··· 21 22 pub use job_list::JobList; 22 23 pub use ls::Ls; 23 24 pub use mkdir::Mkdir; 25 + pub use mv::Mv; 24 26 pub use open::Open; 25 27 pub use pwd::Pwd; 26 28 pub use random::Random;
+145
src/cmd/mv.rs
··· 1 + use std::io::{Read, Write}; 2 + 3 + use crate::globals::{get_pwd, to_shell_err}; 4 + use nu_engine::CallExt; 5 + use nu_protocol::{ 6 + Category, PipelineData, ShellError, Signature, SyntaxShape, Type, 7 + engine::{Command, EngineState, Stack}, 8 + }; 9 + use vfs::{VfsError, VfsFileType}; 10 + 11 + #[derive(Clone)] 12 + pub struct Mv; 13 + 14 + impl Command for Mv { 15 + fn name(&self) -> &str { 16 + "mv" 17 + } 18 + 19 + fn signature(&self) -> Signature { 20 + Signature::build("mv") 21 + .required( 22 + "source", 23 + SyntaxShape::Filepath, 24 + "path to the file or directory to move", 25 + ) 26 + .required( 27 + "destination", 28 + SyntaxShape::Filepath, 29 + "path to the destination", 30 + ) 31 + .input_output_type(Type::Nothing, Type::Nothing) 32 + .category(Category::FileSystem) 33 + } 34 + 35 + fn description(&self) -> &str { 36 + "move a file or directory in the virtual filesystem." 37 + } 38 + 39 + fn run( 40 + &self, 41 + engine_state: &EngineState, 42 + stack: &mut Stack, 43 + call: &nu_protocol::engine::Call, 44 + _input: PipelineData, 45 + ) -> Result<PipelineData, ShellError> { 46 + let source_path: String = call.req(engine_state, stack, 0)?; 47 + let dest_path: String = call.req(engine_state, stack, 1)?; 48 + 49 + // Prevent moving root 50 + if source_path == "/" { 51 + return Err(ShellError::GenericError { 52 + error: "cannot move root".to_string(), 53 + msg: "refusing to move root directory".to_string(), 54 + span: Some(call.arguments_span()), 55 + help: None, 56 + inner: vec![], 57 + }); 58 + } 59 + 60 + // Resolve source relative to PWD (or absolute if path starts with '/') 61 + let source = get_pwd() 62 + .join(source_path.trim_end_matches('/')) 63 + .map_err(to_shell_err(call.arguments_span()))?; 64 + 65 + // Resolve destination relative to PWD (or absolute if path starts with '/') 66 + let dest = get_pwd() 67 + .join(dest_path.trim_end_matches('/')) 68 + .map_err(to_shell_err(call.arguments_span()))?; 69 + 70 + // Check that source exists 71 + let meta = source.metadata().map_err(to_shell_err(call.arguments_span()))?; 72 + 73 + match meta.file_type { 74 + VfsFileType::File => move_file(&source, &dest, call.arguments_span())?, 75 + VfsFileType::Directory => move_directory(&source, &dest, call.arguments_span())?, 76 + } 77 + 78 + Ok(PipelineData::Empty) 79 + } 80 + } 81 + 82 + fn move_file( 83 + source: &vfs::VfsPath, 84 + dest: &vfs::VfsPath, 85 + span: nu_protocol::Span, 86 + ) -> Result<(), ShellError> { 87 + // Read source file content 88 + let mut source_file = source 89 + .open_file() 90 + .map_err(to_shell_err(span))?; 91 + 92 + let mut contents = Vec::new(); 93 + source_file 94 + .read_to_end(&mut contents) 95 + .map_err(|e| ShellError::GenericError { 96 + error: "io error".to_string(), 97 + msg: format!("failed to read source file: {}", e), 98 + span: Some(span), 99 + help: None, 100 + inner: vec![], 101 + })?; 102 + 103 + // Create destination file and write content 104 + dest.create_file() 105 + .map_err(to_shell_err(span)) 106 + .and_then(|mut f| { 107 + f.write_all(&contents) 108 + .map_err(VfsError::from) 109 + .map_err(to_shell_err(span)) 110 + })?; 111 + 112 + // Remove source file 113 + source.remove_file().map_err(to_shell_err(span))?; 114 + 115 + Ok(()) 116 + } 117 + 118 + fn move_directory( 119 + source: &vfs::VfsPath, 120 + dest: &vfs::VfsPath, 121 + span: nu_protocol::Span, 122 + ) -> Result<(), ShellError> { 123 + // Try to create destination directory (create_dir_all handles parent creation) 124 + // If it already exists, that's fine - we'll move entries into it 125 + let _ = dest.create_dir_all().map_err(to_shell_err(span)); 126 + 127 + // Recursively move all entries 128 + let entries = source.read_dir().map_err(to_shell_err(span))?; 129 + for entry_name in entries { 130 + let source_entry = source.join(entry_name.as_str()).map_err(to_shell_err(span))?; 131 + let dest_entry = dest.join(entry_name.as_str()).map_err(to_shell_err(span))?; 132 + 133 + let entry_meta = source_entry.metadata().map_err(to_shell_err(span))?; 134 + match entry_meta.file_type { 135 + VfsFileType::File => move_file(&source_entry, &dest_entry, span)?, 136 + VfsFileType::Directory => move_directory(&source_entry, &dest_entry, span)?, 137 + } 138 + } 139 + 140 + // Remove source directory 141 + source.remove_dir_all().map_err(to_shell_err(span))?; 142 + 143 + Ok(()) 144 + } 145 +
+3 -2
src/lib.rs
··· 31 31 32 32 use crate::{ 33 33 cmd::{ 34 - Cd, Fetch, Job, JobKill, JobList, Ls, Mkdir, Open, Pwd, Random, Rm, Save, Source, Sys, 34 + Cd, Fetch, Job, JobKill, JobList, Ls, Mkdir, Mv, Open, Pwd, Random, Rm, Save, Source, Sys, 35 35 Version, 36 36 }, 37 37 default_context::add_shell_command_context, ··· 134 134 write_file("welcome.txt", &welcome_txt)?; 135 135 136 136 let mut working_set = StateWorkingSet::new(&engine_state); 137 - let decls: [Box<dyn Command>; 15] = [ 137 + let decls: [Box<dyn Command>; 16] = [ 138 138 Box::new(Ls), 139 139 Box::new(Open), 140 140 Box::new(Save), 141 141 Box::new(Mkdir), 142 + Box::new(Mv), 142 143 Box::new(Pwd), 143 144 Box::new(Cd), 144 145 Box::new(Rm),