+2
src/cmd/mod.rs
+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
+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
+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),