nu_command/filesystem/
ucp.rs

1use nu_engine::command_prelude::*;
2use nu_glob::MatchOptions;
3use nu_protocol::{
4    NuGlob,
5    shell_error::{self, io::IoError},
6};
7use std::path::PathBuf;
8use uu_cp::{BackupMode, CopyMode, CpError, UpdateMode};
9use uucore::{localized_help_template, translate};
10
11// TODO: related to uucore::error::set_exit_code(EXIT_ERR)
12// const EXIT_ERR: i32 = 1;
13
14#[cfg(not(target_os = "windows"))]
15const PATH_SEPARATOR: &str = "/";
16#[cfg(target_os = "windows")]
17const PATH_SEPARATOR: &str = "\\";
18
19#[derive(Clone)]
20pub struct UCp;
21
22impl Command for UCp {
23    fn name(&self) -> &str {
24        "cp"
25    }
26
27    fn description(&self) -> &str {
28        "Copy files using uutils/coreutils cp."
29    }
30
31    fn search_terms(&self) -> Vec<&str> {
32        vec!["copy", "file", "files", "coreutils"]
33    }
34
35    fn signature(&self) -> Signature {
36        Signature::build("cp")
37            .input_output_types(vec![(Type::Nothing, Type::Nothing)])
38            .switch("recursive", "copy directories recursively", Some('r'))
39            .switch("verbose", "explicitly state what is being done", Some('v'))
40            .switch(
41                "force",
42                "if an existing destination file cannot be opened, remove it and try
43                    again (this option is ignored when the -n option is also used).
44                    currently not implemented for windows",
45                Some('f'),
46            )
47            .switch("interactive", "ask before overwriting files", Some('i'))
48            .switch(
49                "update",
50                "copy only when the SOURCE file is newer than the destination file or when the destination file is missing",
51                Some('u')
52            )
53            .switch("progress", "display a progress bar", Some('p'))
54            .switch("no-clobber", "do not overwrite an existing file", Some('n'))
55            .named(
56                "preserve",
57                SyntaxShape::List(Box::new(SyntaxShape::String)),
58                "preserve only the specified attributes (empty list means no attributes preserved)
59                    if not specified only mode is preserved
60                    possible values: mode, ownership (unix only), timestamps, context, link, links, xattr",
61                None
62            )
63            .switch("debug", "explain how a file is copied. Implies -v", None)
64            .switch("all", "move hidden files if '*' is provided", Some('a'))
65            .rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "Copy SRC file/s to DEST.")
66            .allow_variants_without_examples(true)
67            .category(Category::FileSystem)
68    }
69
70    fn examples(&self) -> Vec<Example<'_>> {
71        vec![
72            Example {
73                description: "Copy myfile to dir_b",
74                example: "cp myfile dir_b",
75                result: None,
76            },
77            Example {
78                description: "Recursively copy dir_a to dir_b",
79                example: "cp -r dir_a dir_b",
80                result: None,
81            },
82            Example {
83                description: "Recursively copy dir_a to dir_b, and print the feedbacks",
84                example: "cp -r -v dir_a dir_b",
85                result: None,
86            },
87            Example {
88                description: "Move many files into a directory",
89                example: "cp *.txt dir_a",
90                result: None,
91            },
92            Example {
93                description: "Copy only if source file is newer than target file",
94                example: "cp -u myfile newfile",
95                result: None,
96            },
97            Example {
98                description: "Copy file preserving mode and timestamps attributes",
99                example: "cp --preserve [ mode timestamps ] myfile newfile",
100                result: None,
101            },
102            Example {
103                description: "Copy file erasing all attributes",
104                example: "cp --preserve [] myfile newfile",
105                result: None,
106            },
107            Example {
108                description: "Copy file to a directory three levels above its current location",
109                example: "cp myfile ....",
110                result: None,
111            },
112        ]
113    }
114
115    fn run(
116        &self,
117        engine_state: &EngineState,
118        stack: &mut Stack,
119        call: &Call,
120        _input: PipelineData,
121    ) -> Result<PipelineData, ShellError> {
122        // setup the uutils error translation
123        let _ = localized_help_template("cp");
124
125        let interactive = call.has_flag(engine_state, stack, "interactive")?;
126        let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
127            (UpdateMode::IfOlder, CopyMode::Update)
128        } else {
129            (UpdateMode::All, CopyMode::Copy)
130        };
131
132        let force = call.has_flag(engine_state, stack, "force")?;
133        let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
134        let progress = call.has_flag(engine_state, stack, "progress")?;
135        let recursive = call.has_flag(engine_state, stack, "recursive")?;
136        let verbose = call.has_flag(engine_state, stack, "verbose")?;
137        let preserve: Option<Value> = call.get_flag(engine_state, stack, "preserve")?;
138        let all = call.has_flag(engine_state, stack, "all")?;
139
140        let debug = call.has_flag(engine_state, stack, "debug")?;
141        let overwrite = if no_clobber {
142            uu_cp::OverwriteMode::NoClobber
143        } else if interactive {
144            if force {
145                uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Force)
146            } else {
147                uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Standard)
148            }
149        } else if force {
150            uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Force)
151        } else {
152            uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Standard)
153        };
154        #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
155        let reflink_mode = uu_cp::ReflinkMode::Auto;
156        #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
157        let reflink_mode = uu_cp::ReflinkMode::Never;
158        let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
159        if paths.is_empty() {
160            return Err(ShellError::GenericError {
161                error: "Missing file operand".into(),
162                msg: "Missing file operand".into(),
163                span: Some(call.head),
164                help: Some("Please provide source and destination paths".into()),
165                inner: vec![],
166            });
167        }
168
169        if paths.len() == 1 {
170            return Err(ShellError::GenericError {
171                error: "Missing destination path".into(),
172                msg: format!(
173                    "Missing destination path operand after {}",
174                    paths[0].item.as_ref()
175                ),
176                span: Some(paths[0].span),
177                help: None,
178                inner: vec![],
179            });
180        }
181        let target = paths.pop().expect("Should not be reached?");
182        let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
183            target.item.to_string(),
184        ));
185        let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
186        let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
187        if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() {
188            return Err(ShellError::GenericError {
189                error: "is not a directory".into(),
190                msg: "is not a directory".into(),
191                span: Some(target.span),
192                help: None,
193                inner: vec![],
194            });
195        };
196
197        // paths now contains the sources
198
199        let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
200        let glob_options = if all {
201            None
202        } else {
203            let glob_options = MatchOptions {
204                require_literal_leading_dot: true,
205                ..Default::default()
206            };
207            Some(glob_options)
208        };
209        for mut p in paths {
210            p.item = p.item.strip_ansi_string_unlikely();
211            let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
212                &p,
213                &cwd,
214                call.head,
215                glob_options,
216                engine_state.signals().clone(),
217            )
218            .map(|f| f.1)?
219            .collect();
220            if exp_files.is_empty() {
221                return Err(ShellError::Io(IoError::new(
222                    shell_error::io::ErrorKind::FileNotFound,
223                    p.span,
224                    PathBuf::from(p.item.to_string()),
225                )));
226            };
227            let mut app_vals: Vec<PathBuf> = Vec::new();
228            for v in exp_files {
229                match v {
230                    Ok(path) => {
231                        if !recursive && path.is_dir() {
232                            return Err(ShellError::GenericError {
233                                error: "could_not_copy_directory".into(),
234                                msg: "resolves to a directory (not copied)".into(),
235                                span: Some(p.span),
236                                help: Some(
237                                    "Directories must be copied using \"--recursive\"".into(),
238                                ),
239                                inner: vec![],
240                            });
241                        };
242                        app_vals.push(path)
243                    }
244                    Err(e) => return Err(e),
245                }
246            }
247            sources.push((app_vals, p.item.is_expand()));
248        }
249
250        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
251        // supported in Nushell
252        for (sources, need_expand_tilde) in sources.iter_mut() {
253            for src in sources.iter_mut() {
254                if !src.is_absolute() {
255                    *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
256                }
257            }
258        }
259        let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
260
261        let attributes = make_attributes(preserve)?;
262
263        let options = uu_cp::Options {
264            overwrite,
265            reflink_mode,
266            recursive,
267            debug,
268            attributes,
269            verbose: verbose || debug,
270            dereference: !recursive,
271            progress_bar: progress,
272            attributes_only: false,
273            backup: BackupMode::None,
274            copy_contents: false,
275            cli_dereference: false,
276            copy_mode,
277            no_target_dir: false,
278            one_file_system: false,
279            parents: false,
280            sparse_mode: uu_cp::SparseMode::Auto,
281            strip_trailing_slashes: false,
282            backup_suffix: String::from("~"),
283            target_dir: None,
284            update,
285            set_selinux_context: false,
286            context: None,
287        };
288
289        if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
290            match error {
291                // code should still be EXIT_ERR as does GNU cp
292                CpError::NotAllFilesCopied => {}
293                _ => {
294                    eprintln!("here");
295                    return Err(ShellError::GenericError {
296                        error: format!("{error}"),
297                        msg: translate!(&error.to_string()),
298                        span: None,
299                        help: None,
300                        inner: vec![],
301                    });
302                }
303            };
304            // TODO: What should we do in place of set_exit_code?
305            // uucore::error::set_exit_code(EXIT_ERR);
306        }
307        Ok(PipelineData::empty())
308    }
309}
310
311const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true };
312const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true };
313
314fn make_attributes(preserve: Option<Value>) -> Result<uu_cp::Attributes, ShellError> {
315    if let Some(preserve) = preserve {
316        let mut attributes = uu_cp::Attributes {
317            #[cfg(any(
318                target_os = "linux",
319                target_os = "freebsd",
320                target_os = "android",
321                target_os = "macos",
322                target_os = "netbsd",
323                target_os = "openbsd"
324            ))]
325            ownership: ATTR_UNSET,
326            mode: ATTR_UNSET,
327            timestamps: ATTR_UNSET,
328            context: ATTR_UNSET,
329            links: ATTR_UNSET,
330            xattr: ATTR_UNSET,
331        };
332        parse_and_set_attributes_list(&preserve, &mut attributes)?;
333
334        Ok(attributes)
335    } else {
336        // By default preseerve only mode
337        Ok(uu_cp::Attributes {
338            mode: ATTR_SET,
339            #[cfg(any(
340                target_os = "linux",
341                target_os = "freebsd",
342                target_os = "android",
343                target_os = "macos",
344                target_os = "netbsd",
345                target_os = "openbsd"
346            ))]
347            ownership: ATTR_UNSET,
348            timestamps: ATTR_UNSET,
349            context: ATTR_UNSET,
350            links: ATTR_UNSET,
351            xattr: ATTR_UNSET,
352        })
353    }
354}
355
356fn parse_and_set_attributes_list(
357    list: &Value,
358    attribute: &mut uu_cp::Attributes,
359) -> Result<(), ShellError> {
360    match list {
361        Value::List { vals, .. } => {
362            for val in vals {
363                parse_and_set_attribute(val, attribute)?;
364            }
365            Ok(())
366        }
367        _ => Err(ShellError::IncompatibleParametersSingle {
368            msg: "--preserve flag expects a list of strings".into(),
369            span: list.span(),
370        }),
371    }
372}
373
374fn parse_and_set_attribute(
375    value: &Value,
376    attribute: &mut uu_cp::Attributes,
377) -> Result<(), ShellError> {
378    match value {
379        Value::String { val, .. } => {
380            let attribute = match val.as_str() {
381                "mode" => &mut attribute.mode,
382                #[cfg(any(
383                    target_os = "linux",
384                    target_os = "freebsd",
385                    target_os = "android",
386                    target_os = "macos",
387                    target_os = "netbsd",
388                    target_os = "openbsd"
389                ))]
390                "ownership" => &mut attribute.ownership,
391                "timestamps" => &mut attribute.timestamps,
392                "context" => &mut attribute.context,
393                "link" | "links" => &mut attribute.links,
394                "xattr" => &mut attribute.xattr,
395                _ => {
396                    return Err(ShellError::IncompatibleParametersSingle {
397                        msg: format!("--preserve flag got an unexpected attribute \"{val}\""),
398                        span: value.span(),
399                    });
400                }
401            };
402            *attribute = ATTR_SET;
403            Ok(())
404        }
405        _ => Err(ShellError::IncompatibleParametersSingle {
406            msg: "--preserve flag expects a list of strings".into(),
407            span: value.span(),
408        }),
409    }
410}
411
412#[cfg(test)]
413mod test {
414    use super::*;
415    #[test]
416    fn test_examples() {
417        use crate::test_examples;
418
419        test_examples(UCp {})
420    }
421}