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