1use nu_engine::command_prelude::*;
2use nu_glob::MatchOptions;
3use nu_path::expand_path_with;
4use nu_protocol::{
5 NuGlob, record,
6 shell_error::{self, generic::GenericError, io::IoError},
7};
8use std::{ffi::OsString, path::PathBuf};
9use uu_mv::{BackupMode, UpdateMode};
10use uucore::{localized_help_template, translate};
11
12#[derive(Clone)]
13pub struct UMv;
14
15impl Command for UMv {
16 fn name(&self) -> &str {
17 "mv"
18 }
19
20 fn description(&self) -> &str {
21 "Move files or directories using uutils/coreutils mv."
22 }
23
24 fn examples(&self) -> Vec<Example<'_>> {
25 vec![
26 Example {
27 description: "Rename a file.",
28 example: "mv before.txt after.txt",
29 result: None,
30 },
31 Example {
32 description: "Move a file into a directory.",
33 example: "mv test.txt my/subdirectory",
34 result: None,
35 },
36 Example {
37 description: "Move only if source file is newer than target file.",
38 example: "mv -u new/test.txt old/",
39 result: None,
40 },
41 Example {
42 description: "Move many files into a directory.",
43 example: "mv *.txt my/subdirectory",
44 result: None,
45 },
46 Example {
47 description: r#"Move a file into the "my" directory two levels up in the directory tree."#,
48 example: "mv test.txt .../my/",
49 result: None,
50 },
51 Example {
52 description: "Move a file and show what is being done.",
53 example: "mv -v before.txt after.txt",
54 result: Some(Value::test_list(vec![
55 record! {
56 "source" => Value::string("before.txt", Span::test_data()),
57 "destination" => Value::string("after.txt", Span::test_data()),
58 "message" => Value::string("renamed ", Span::test_data()),
59 }
60 .into_value(Span::test_data()),
61 ])),
62 },
63 ]
64 }
65
66 fn search_terms(&self) -> Vec<&str> {
67 vec!["move", "file", "files", "coreutils", "rename", "ren"]
68 }
69
70 fn signature(&self) -> nu_protocol::Signature {
71 Signature::build("mv")
72 .input_output_types(vec![
73 (
74 Type::Nothing,
75 Type::Table(
76 [
77 ("source".into(), Type::String),
78 ("destination".into(), Type::String),
79 ("message".into(), Type::String),
80 ]
81 .into(),
82 ),
83 ),
84 (Type::Nothing, Type::Nothing),
85 ])
86 .switch("force", "Do not prompt before overwriting.", Some('f'))
87 .switch("verbose", "Explain what is being done.", Some('v'))
88 .switch("progress", "Display a progress bar.", Some('p'))
89 .switch("interactive", "Prompt before overwriting.", Some('i'))
90 .switch(
91 "update",
92 "Move and overwrite only when the SOURCE file is newer than the destination file or when the destination file is missing.",
93 Some('u')
94 )
95 .switch("no-clobber", "Do not overwrite an existing file.", Some('n'))
96 .switch("all", "Move hidden files if '*' is provided.", Some('a'))
97 .rest(
98 "paths",
99 SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
100 "Rename SRC to DST, or move SRC to DIR.",
101 )
102 .allow_variants_without_examples(true)
103 .category(Category::FileSystem)
104 }
105
106 fn run(
107 &self,
108 engine_state: &EngineState,
109 stack: &mut Stack,
110 call: &Call,
111 _input: PipelineData,
112 ) -> Result<PipelineData, ShellError> {
113 let _ = localized_help_template("mv");
115
116 let interactive = call.has_flag(engine_state, stack, "interactive")?;
117 let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
118 let progress = call.has_flag(engine_state, stack, "progress")?;
119 let verbose = call.has_flag(engine_state, stack, "verbose")?;
120 let all = call.has_flag(engine_state, stack, "all")?;
121 let overwrite = if no_clobber {
122 uu_mv::OverwriteMode::NoClobber
123 } else if interactive {
124 uu_mv::OverwriteMode::Interactive
125 } else {
126 uu_mv::OverwriteMode::Force
127 };
128 let update = if call.has_flag(engine_state, stack, "update")? {
129 UpdateMode::IfOlder
130 } else {
131 UpdateMode::All
132 };
133
134 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
135 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
136 if paths.is_empty() {
137 return Err(ShellError::Generic(
138 GenericError::new("Missing file operand", "Missing file operand", call.head)
139 .with_help("Please provide source and destination paths"),
140 ));
141 }
142 if paths.len() == 1 {
143 return Err(ShellError::Generic(GenericError::new(
145 "Missing destination path",
146 format!(
147 "Missing destination path operand after {}",
148 expand_path_with(paths[0].item.as_ref(), cwd, paths[0].item.is_expand())
149 .to_string_lossy()
150 ),
151 paths[0].span,
152 )));
153 }
154
155 let spanned_target = paths.pop().ok_or(ShellError::NushellFailedSpanned {
157 msg: "Missing file operand".into(),
158 label: "Missing file operand".into(),
159 span: call.head,
160 })?;
161 let mut files: Vec<(Vec<PathBuf>, bool)> = Vec::new();
162 let glob_options = if all {
163 None
164 } else {
165 let glob_options = MatchOptions {
166 require_literal_leading_dot: true,
167 ..Default::default()
168 };
169 Some(glob_options)
170 };
171 for mut p in paths {
172 p.item = p.item.strip_ansi_string_unlikely();
173 let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
174 &p,
175 &cwd,
176 call.head,
177 glob_options,
178 engine_state.signals().clone(),
179 )
180 .map(|f| f.1)?
181 .collect();
182 if exp_files.is_empty() {
183 return Err(ShellError::Io(IoError::new(
184 shell_error::io::ErrorKind::FileNotFound,
185 p.span,
186 PathBuf::from(p.item.to_string()),
187 )));
188 };
189 let mut app_vals: Vec<PathBuf> = Vec::new();
190 for v in exp_files {
191 match v {
192 Ok(path) => {
193 app_vals.push(path);
194 }
195 Err(e) => return Err(e),
196 }
197 }
198 files.push((app_vals, p.item.is_expand()));
199 }
200
201 for (files, need_expand_tilde) in files.iter_mut() {
204 for src in files.iter_mut() {
205 if !src.is_absolute() {
206 *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
207 }
208 }
209 }
210 let source_files: Vec<PathBuf> = files.into_iter().flat_map(|x| x.0).collect();
211
212 let abs_target_path = expand_path_with(
214 nu_utils::strip_ansi_string_unlikely(spanned_target.item.to_string()),
215 &cwd,
216 matches!(spanned_target.item, NuGlob::Expand(..)),
217 );
218
219 let verbose_msgs: Vec<(PathBuf, PathBuf)> = if verbose {
221 source_files
222 .iter()
223 .map(|src| {
224 let dest = if abs_target_path.is_dir() {
225 abs_target_path.join(src.file_name().unwrap_or_default())
226 } else {
227 abs_target_path.clone()
228 };
229 (src.clone(), dest)
230 })
231 .collect()
232 } else {
233 Vec::new()
234 };
235 let mut files_for_mv = source_files;
236 files_for_mv.push(abs_target_path.clone());
237 let files_for_mv = files_for_mv
238 .into_iter()
239 .map(|p| p.into_os_string())
240 .collect::<Vec<OsString>>();
241 let options = uu_mv::Options {
242 overwrite,
243 progress_bar: progress,
244 verbose: false,
245 suffix: String::from("~"),
246 backup: BackupMode::None,
247 update,
248 target_dir: None,
249 no_target_dir: false,
250 strip_slashes: false,
251 debug: false,
252 context: None,
253 };
254 if let Err(error) = uu_mv::mv(&files_for_mv, &options) {
255 return Err(ShellError::Generic(GenericError::new_internal(
256 format!("{error}"),
257 translate!(&error.to_string()),
258 )));
259 }
260
261 if verbose {
262 let output: Vec<Value> = verbose_msgs
263 .into_iter()
264 .map(|(src, dest)| {
265 record! {
266 "source" => Value::string(src.display().to_string(), call.head),
267 "destination" => Value::string(dest.display().to_string(), call.head),
268 "message" => Value::string(translate!(
269 "mv-verbose-renamed",
270 "from" => src.display().to_string(),
271 "to" => dest.display().to_string(),
272 ), call.head)
273 }
274 .into_value(call.head)
275 })
276 .collect();
277 Ok(PipelineData::Value(Value::list(output, call.head), None))
278 } else {
279 Ok(PipelineData::empty())
280 }
281 }
282}