Skip to main content

nu_command/filesystem/
umv.rs

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        // setup the uutils error translation
114        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            // expand path for better error message
144            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        // Do not glob target
156        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        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
202        // supported in Nushell
203        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        // Add back the target after globbing
213        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        // Collect verbose messages before the move
220        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}