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                        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        // 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                let path = v?;
192                app_vals.push(path);
193            }
194            files.push((app_vals, p.item.is_expand()));
195        }
196
197        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
198        // supported in Nushell
199        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        // Add back the target after globbing
209        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        // Collect verbose messages before the move
216        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}