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 vec![
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 let path = v?;
192 app_vals.push(path);
193 }
194 files.push((app_vals, p.item.is_expand()));
195 }
196
197 for (files, need_expand_tilde) in files.iter_mut() {
200 for src in files.iter_mut() {
201 if !src.is_absolute() {
202 *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
203 }
204 }
205 }
206 let source_files: Vec<PathBuf> = files.into_iter().flat_map(|x| x.0).collect();
207
208 let abs_target_path = expand_path_with(
210 nu_utils::strip_ansi_string_unlikely(spanned_target.item.to_string()),
211 &cwd,
212 matches!(spanned_target.item, NuGlob::Expand(..)),
213 );
214
215 let verbose_msgs: Vec<(PathBuf, PathBuf)> = if verbose {
217 source_files
218 .iter()
219 .map(|src| {
220 let dest = if abs_target_path.is_dir() {
221 abs_target_path.join(src.file_name().unwrap_or_default())
222 } else {
223 abs_target_path.clone()
224 };
225 (src.clone(), dest)
226 })
227 .collect()
228 } else {
229 Vec::new()
230 };
231 let mut files_for_mv = source_files;
232 files_for_mv.push(abs_target_path.clone());
233 let files_for_mv = files_for_mv
234 .into_iter()
235 .map(|p| p.into_os_string())
236 .collect::<Vec<OsString>>();
237 let options = uu_mv::Options {
238 overwrite,
239 progress_bar: progress,
240 verbose: false,
241 suffix: String::from("~"),
242 backup: BackupMode::None,
243 update,
244 target_dir: None,
245 no_target_dir: false,
246 strip_slashes: false,
247 debug: false,
248 context: None,
249 exchange: false,
250 };
251 if let Err(error) = uu_mv::mv(&files_for_mv, &options) {
252 return Err(ShellError::Generic(GenericError::new_internal(
253 format!("{error}"),
254 translate!(&error.to_string()),
255 )));
256 }
257
258 if verbose {
259 let output: Vec<Value> = verbose_msgs
260 .into_iter()
261 .map(|(src, dest)| {
262 record! {
263 "source" => Value::string(src.display().to_string(), call.head),
264 "destination" => Value::string(dest.display().to_string(), call.head),
265 "message" => Value::string(translate!(
266 "mv-verbose-renamed",
267 "from" => src.display().to_string(),
268 "to" => dest.display().to_string(),
269 ), call.head)
270 }
271 .into_value(call.head)
272 })
273 .collect();
274 Ok(PipelineData::Value(Value::list(output, call.head), None))
275 } else {
276 Ok(PipelineData::empty())
277 }
278 }
279}