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,
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        ]
52    }
53
54    fn search_terms(&self) -> Vec<&str> {
55        vec!["move", "file", "files", "coreutils", "rename", "ren"]
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::Generic(
113                GenericError::new("Missing file operand", "Missing file operand", call.head)
114                    .with_help("Please provide source and destination paths"),
115            ));
116        }
117        if paths.len() == 1 {
118            // expand path for better error message
119            return Err(ShellError::Generic(GenericError::new(
120                "Missing destination path",
121                format!(
122                    "Missing destination path operand after {}",
123                    expand_path_with(paths[0].item.as_ref(), cwd, paths[0].item.is_expand())
124                        .to_string_lossy()
125                ),
126                paths[0].span,
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        let glob_options = if all {
138            None
139        } else {
140            let glob_options = MatchOptions {
141                require_literal_leading_dot: true,
142                ..Default::default()
143            };
144            Some(glob_options)
145        };
146        for mut p in paths {
147            p.item = p.item.strip_ansi_string_unlikely();
148            let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
149                &p,
150                &cwd,
151                call.head,
152                glob_options,
153                engine_state.signals().clone(),
154            )
155            .map(|f| f.1)?
156            .collect();
157            if exp_files.is_empty() {
158                return Err(ShellError::Io(IoError::new(
159                    shell_error::io::ErrorKind::FileNotFound,
160                    p.span,
161                    PathBuf::from(p.item.to_string()),
162                )));
163            };
164            let mut app_vals: Vec<PathBuf> = Vec::new();
165            for v in exp_files {
166                match v {
167                    Ok(path) => {
168                        app_vals.push(path);
169                    }
170                    Err(e) => return Err(e),
171                }
172            }
173            files.push((app_vals, p.item.is_expand()));
174        }
175
176        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
177        // supported in Nushell
178        for (files, need_expand_tilde) in files.iter_mut() {
179            for src in files.iter_mut() {
180                if !src.is_absolute() {
181                    *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
182                }
183            }
184        }
185        let mut files: Vec<PathBuf> = files.into_iter().flat_map(|x| x.0).collect();
186
187        // Add back the target after globbing
188        let abs_target_path = expand_path_with(
189            nu_utils::strip_ansi_string_unlikely(spanned_target.item.to_string()),
190            &cwd,
191            matches!(spanned_target.item, NuGlob::Expand(..)),
192        );
193        files.push(abs_target_path.clone());
194        let files = files
195            .into_iter()
196            .map(|p| p.into_os_string())
197            .collect::<Vec<OsString>>();
198        let options = uu_mv::Options {
199            overwrite,
200            progress_bar: progress,
201            verbose,
202            suffix: String::from("~"),
203            backup: BackupMode::None,
204            update,
205            target_dir: None,
206            no_target_dir: false,
207            strip_slashes: false,
208            debug: false,
209            context: None,
210        };
211        if let Err(error) = uu_mv::mv(&files, &options) {
212            return Err(ShellError::Generic(GenericError::new_internal(
213                format!("{error}"),
214                translate!(&error.to_string()),
215            )));
216        }
217        Ok(PipelineData::empty())
218    }
219}