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};
10
11#[derive(Clone)]
12pub struct UMv;
13
14impl Command for UMv {
15    fn name(&self) -> &str {
16        "mv"
17    }
18
19    fn description(&self) -> &str {
20        "Move files or directories using uutils/coreutils mv."
21    }
22
23    fn examples(&self) -> Vec<Example> {
24        vec![
25            Example {
26                description: "Rename a file",
27                example: "mv before.txt after.txt",
28                result: None,
29            },
30            Example {
31                description: "Move a file into a directory",
32                example: "mv test.txt my/subdirectory",
33                result: None,
34            },
35            Example {
36                description: "Move only if source file is newer than target file",
37                example: "mv -u new/test.txt old/",
38                result: None,
39            },
40            Example {
41                description: "Move many files into a directory",
42                example: "mv *.txt my/subdirectory",
43                result: None,
44            },
45            Example {
46                description: r#"Move a file into the "my" directory two levels up in the directory tree"#,
47                example: "mv test.txt .../my/",
48                result: None,
49            },
50        ]
51    }
52
53    fn search_terms(&self) -> Vec<&str> {
54        vec!["move", "file", "files", "coreutils"]
55    }
56
57    fn signature(&self) -> nu_protocol::Signature {
58        Signature::build("mv")
59            .input_output_types(vec![(Type::Nothing, Type::Nothing)])
60            .switch("force", "do not prompt before overwriting", Some('f'))
61            .switch("verbose", "explain what is being done.", Some('v'))
62            .switch("progress", "display a progress bar", Some('p'))
63            .switch("interactive", "prompt before overwriting", Some('i'))
64            .switch(
65                "update",
66                "move and overwrite only when the SOURCE file is newer than the destination file or when the destination file is missing",
67                Some('u')
68            )
69            .switch("no-clobber", "do not overwrite an existing file", Some('n'))
70            .rest(
71                "paths",
72                SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
73                "Rename SRC to DST, or move SRC to DIR.",
74            )
75            .allow_variants_without_examples(true)
76            .category(Category::FileSystem)
77    }
78
79    fn run(
80        &self,
81        engine_state: &EngineState,
82        stack: &mut Stack,
83        call: &Call,
84        _input: PipelineData,
85    ) -> Result<PipelineData, ShellError> {
86        let interactive = call.has_flag(engine_state, stack, "interactive")?;
87        let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
88        let progress = call.has_flag(engine_state, stack, "progress")?;
89        let verbose = call.has_flag(engine_state, stack, "verbose")?;
90        let overwrite = if no_clobber {
91            uu_mv::OverwriteMode::NoClobber
92        } else if interactive {
93            uu_mv::OverwriteMode::Interactive
94        } else {
95            uu_mv::OverwriteMode::Force
96        };
97        let update = if call.has_flag(engine_state, stack, "update")? {
98            UpdateMode::ReplaceIfOlder
99        } else {
100            UpdateMode::ReplaceAll
101        };
102
103        #[allow(deprecated)]
104        let cwd = current_dir(engine_state, stack)?;
105        let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
106        if paths.is_empty() {
107            return Err(ShellError::GenericError {
108                error: "Missing file operand".into(),
109                msg: "Missing file operand".into(),
110                span: Some(call.head),
111                help: Some("Please provide source and destination paths".into()),
112                inner: Vec::new(),
113            });
114        }
115        if paths.len() == 1 {
116            // expand path for better error message
117            return Err(ShellError::GenericError {
118                error: "Missing destination path".into(),
119                msg: format!(
120                    "Missing destination path operand after {}",
121                    expand_path_with(paths[0].item.as_ref(), cwd, paths[0].item.is_expand())
122                        .to_string_lossy()
123                ),
124                span: Some(paths[0].span),
125                help: None,
126                inner: Vec::new(),
127            });
128        }
129
130        // Do not glob target
131        let spanned_target = paths.pop().ok_or(ShellError::NushellFailedSpanned {
132            msg: "Missing file operand".into(),
133            label: "Missing file operand".into(),
134            span: call.head,
135        })?;
136        let mut files: Vec<(Vec<PathBuf>, bool)> = Vec::new();
137        for mut p in paths {
138            p.item = p.item.strip_ansi_string_unlikely();
139            let exp_files: Vec<Result<PathBuf, ShellError>> =
140                nu_engine::glob_from(&p, &cwd, call.head, None, engine_state.signals().clone())
141                    .map(|f| f.1)?
142                    .collect();
143            if exp_files.is_empty() {
144                return Err(ShellError::Io(IoError::new(
145                    shell_error::io::ErrorKind::FileNotFound,
146                    p.span,
147                    PathBuf::from(p.item.to_string()),
148                )));
149            };
150            let mut app_vals: Vec<PathBuf> = Vec::new();
151            for v in exp_files {
152                match v {
153                    Ok(path) => {
154                        app_vals.push(path);
155                    }
156                    Err(e) => return Err(e),
157                }
158            }
159            files.push((app_vals, p.item.is_expand()));
160        }
161
162        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
163        // supported in Nushell
164        for (files, need_expand_tilde) in files.iter_mut() {
165            for src in files.iter_mut() {
166                if !src.is_absolute() {
167                    *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
168                }
169            }
170        }
171        let mut files: Vec<PathBuf> = files.into_iter().flat_map(|x| x.0).collect();
172
173        // Add back the target after globbing
174        let abs_target_path = expand_path_with(
175            nu_utils::strip_ansi_string_unlikely(spanned_target.item.to_string()),
176            &cwd,
177            matches!(spanned_target.item, NuGlob::Expand(..)),
178        );
179        files.push(abs_target_path.clone());
180        let files = files
181            .into_iter()
182            .map(|p| p.into_os_string())
183            .collect::<Vec<OsString>>();
184        let options = uu_mv::Options {
185            overwrite,
186            progress_bar: progress,
187            verbose,
188            suffix: String::from("~"),
189            backup: BackupMode::NoBackup,
190            update,
191            target_dir: None,
192            no_target_dir: false,
193            strip_slashes: false,
194            debug: false,
195        };
196        if let Err(error) = uu_mv::mv(&files, &options) {
197            return Err(ShellError::GenericError {
198                error: format!("{}", error),
199                msg: format!("{}", error),
200                span: None,
201                help: None,
202                inner: Vec::new(),
203            });
204        }
205        Ok(PipelineData::empty())
206    }
207}