nu_command/filesystem/
ucp.rs

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