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,
6    shell_error::{self, 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        ]
52    }
53
54    fn search_terms(&self) -> Vec<&str> {
55        vec!["move", "file", "files", "coreutils"]
56    }
57
58    fn signature(&self) -> nu_protocol::Signature {
59        Signature::build("mv")
60            .input_output_types(vec![(Type::Nothing, Type::Nothing)])
61            .switch("force", "do not prompt before overwriting", Some('f'))
62            .switch("verbose", "explain what is being done.", Some('v'))
63            .switch("progress", "display a progress bar", Some('p'))
64            .switch("interactive", "prompt before overwriting", Some('i'))
65            .switch(
66                "update",
67                "move and overwrite only when the SOURCE file is newer than the destination file or when the destination file is missing",
68                Some('u')
69            )
70            .switch("no-clobber", "do not overwrite an existing file", Some('n'))
71            .switch("all", "move hidden files if '*' is provided", Some('a'))
72            .rest(
73                "paths",
74                SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
75                "Rename SRC to DST, or move SRC to DIR.",
76            )
77            .allow_variants_without_examples(true)
78            .category(Category::FileSystem)
79    }
80
81    fn run(
82        &self,
83        engine_state: &EngineState,
84        stack: &mut Stack,
85        call: &Call,
86        _input: PipelineData,
87    ) -> Result<PipelineData, ShellError> {
88        // setup the uutils error translation
89        let _ = localized_help_template("mv");
90
91        let interactive = call.has_flag(engine_state, stack, "interactive")?;
92        let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
93        let progress = call.has_flag(engine_state, stack, "progress")?;
94        let verbose = call.has_flag(engine_state, stack, "verbose")?;
95        let all = call.has_flag(engine_state, stack, "all")?;
96        let overwrite = if no_clobber {
97            uu_mv::OverwriteMode::NoClobber
98        } else if interactive {
99            uu_mv::OverwriteMode::Interactive
100        } else {
101            uu_mv::OverwriteMode::Force
102        };
103        let update = if call.has_flag(engine_state, stack, "update")? {
104            UpdateMode::IfOlder
105        } else {
106            UpdateMode::All
107        };
108
109        let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
110        let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
111        if paths.is_empty() {
112            return Err(ShellError::GenericError {
113                error: "Missing file operand".into(),
114                msg: "Missing file operand".into(),
115                span: Some(call.head),
116                help: Some("Please provide source and destination paths".into()),
117                inner: Vec::new(),
118            });
119        }
120        if paths.len() == 1 {
121            // expand path for better error message
122            return Err(ShellError::GenericError {
123                error: "Missing destination path".into(),
124                msg: format!(
125                    "Missing destination path operand after {}",
126                    expand_path_with(paths[0].item.as_ref(), cwd, paths[0].item.is_expand())
127                        .to_string_lossy()
128                ),
129                span: Some(paths[0].span),
130                help: None,
131                inner: Vec::new(),
132            });
133        }
134
135        // Do not glob target
136        let spanned_target = paths.pop().ok_or(ShellError::NushellFailedSpanned {
137            msg: "Missing file operand".into(),
138            label: "Missing file operand".into(),
139            span: call.head,
140        })?;
141        let mut files: Vec<(Vec<PathBuf>, bool)> = Vec::new();
142        let glob_options = if all {
143            None
144        } else {
145            let glob_options = MatchOptions {
146                require_literal_leading_dot: true,
147                ..Default::default()
148            };
149            Some(glob_options)
150        };
151        for mut p in paths {
152            p.item = p.item.strip_ansi_string_unlikely();
153            let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
154                &p,
155                &cwd,
156                call.head,
157                glob_options,
158                engine_state.signals().clone(),
159            )
160            .map(|f| f.1)?
161            .collect();
162            if exp_files.is_empty() {
163                return Err(ShellError::Io(IoError::new(
164                    shell_error::io::ErrorKind::FileNotFound,
165                    p.span,
166                    PathBuf::from(p.item.to_string()),
167                )));
168            };
169            let mut app_vals: Vec<PathBuf> = Vec::new();
170            for v in exp_files {
171                match v {
172                    Ok(path) => {
173                        app_vals.push(path);
174                    }
175                    Err(e) => return Err(e),
176                }
177            }
178            files.push((app_vals, p.item.is_expand()));
179        }
180
181        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
182        // supported in Nushell
183        for (files, need_expand_tilde) in files.iter_mut() {
184            for src in files.iter_mut() {
185                if !src.is_absolute() {
186                    *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
187                }
188            }
189        }
190        let mut files: Vec<PathBuf> = files.into_iter().flat_map(|x| x.0).collect();
191
192        // Add back the target after globbing
193        let abs_target_path = expand_path_with(
194            nu_utils::strip_ansi_string_unlikely(spanned_target.item.to_string()),
195            &cwd,
196            matches!(spanned_target.item, NuGlob::Expand(..)),
197        );
198        files.push(abs_target_path.clone());
199        let files = files
200            .into_iter()
201            .map(|p| p.into_os_string())
202            .collect::<Vec<OsString>>();
203        let options = uu_mv::Options {
204            overwrite,
205            progress_bar: progress,
206            verbose,
207            suffix: String::from("~"),
208            backup: BackupMode::None,
209            update,
210            target_dir: None,
211            no_target_dir: false,
212            strip_slashes: false,
213            debug: false,
214            context: None,
215        };
216        if let Err(error) = uu_mv::mv(&files, &options) {
217            return Err(ShellError::GenericError {
218                error: format!("{error}"),
219                msg: translate!(&error.to_string()),
220                span: None,
221                help: None,
222                inner: Vec::new(),
223            });
224        }
225        Ok(PipelineData::empty())
226    }
227}