Skip to main content

nu_command/filesystem/
ucp.rs

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