nu_command/filesystem/
umv.rs

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