nu_command/filesystem/
ucp.rs

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