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::{MAIN_SEPARATOR, 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#[derive(Clone)]
15pub struct UCp;
16
17impl Command for UCp {
18    fn name(&self) -> &str {
19        "cp"
20    }
21
22    fn description(&self) -> &str {
23        "Copy files using uutils/coreutils cp."
24    }
25
26    fn search_terms(&self) -> Vec<&str> {
27        vec!["copy", "file", "files", "coreutils"]
28    }
29
30    fn signature(&self) -> Signature {
31        Signature::build("cp")
32            .input_output_types(vec![(Type::Nothing, Type::Nothing)])
33            .switch("recursive", "Copy directories recursively.", Some('r'))
34            .switch(
35                "no-dereference",
36                "Copy symbolic links as symbolic links instead of their targets.",
37                Some('P'),
38            )
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", "Copy 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 a symbolic link without copying the target.",
109                example: "cp --no-dereference link-to-file newlink",
110                result: None,
111            },
112            Example {
113                description: "Copy file to a directory three levels above its current location.",
114                example: "cp myfile ....",
115                result: None,
116            },
117        ]
118    }
119
120    fn run(
121        &self,
122        engine_state: &EngineState,
123        stack: &mut Stack,
124        call: &Call,
125        _input: PipelineData,
126    ) -> Result<PipelineData, ShellError> {
127        // setup the uutils error translation
128        let _ = localized_help_template("cp");
129
130        let interactive = call.has_flag(engine_state, stack, "interactive")?;
131        let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
132            (UpdateMode::IfOlder, CopyMode::Update)
133        } else {
134            (UpdateMode::All, CopyMode::Copy)
135        };
136
137        let force = call.has_flag(engine_state, stack, "force")?;
138        let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
139        let progress = call.has_flag(engine_state, stack, "progress")?;
140        let recursive = call.has_flag(engine_state, stack, "recursive")?;
141        let no_dereference = call.has_flag(engine_state, stack, "no-dereference")?;
142        let verbose = call.has_flag(engine_state, stack, "verbose")?;
143        let preserve: Option<Value> = call.get_flag(engine_state, stack, "preserve")?;
144        let all = call.has_flag(engine_state, stack, "all")?;
145
146        let debug = call.has_flag(engine_state, stack, "debug")?;
147        let overwrite = if no_clobber {
148            uu_cp::OverwriteMode::NoClobber
149        } else if interactive {
150            if force {
151                uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Force)
152            } else {
153                uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Standard)
154            }
155        } else if force {
156            uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Force)
157        } else {
158            uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Standard)
159        };
160        #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
161        let reflink_mode = uu_cp::ReflinkMode::Auto;
162        #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
163        let reflink_mode = uu_cp::ReflinkMode::Never;
164        let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
165        if paths.is_empty() {
166            return Err(ShellError::Generic(
167                GenericError::new("Missing file operand", "Missing file operand", call.head)
168                    .with_help("Please provide source and destination paths"),
169            ));
170        }
171
172        if paths.len() == 1 {
173            return Err(ShellError::Generic(GenericError::new(
174                "Missing destination path",
175                format!(
176                    "Missing destination path operand after {}",
177                    paths[0].item.as_ref()
178                ),
179                paths[0].span,
180            )));
181        }
182        let target = paths.pop().expect("Should not be reached?");
183        let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
184            target.item.to_string(),
185        ));
186        let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
187        let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
188        if target.item.as_ref().ends_with(MAIN_SEPARATOR) && !target_path.is_dir() {
189            return Err(ShellError::Generic(GenericError::new(
190                "is not a directory",
191                "is not a directory",
192                target.span,
193            )));
194        };
195
196        // paths now contains the sources
197
198        let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
199        let glob_options = if all {
200            None
201        } else {
202            let glob_options = MatchOptions {
203                require_literal_leading_dot: true,
204                ..Default::default()
205            };
206            Some(glob_options)
207        };
208        for mut p in paths {
209            p.item = p.item.strip_ansi_string_unlikely();
210            let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
211                &p,
212                &cwd,
213                call.head,
214                glob_options,
215                engine_state.signals().clone(),
216            )
217            .map(|f| f.1)?
218            .collect();
219            if exp_files.is_empty() {
220                return Err(ShellError::Io(IoError::new(
221                    shell_error::io::ErrorKind::FileNotFound,
222                    p.span,
223                    PathBuf::from(p.item.to_string()),
224                )));
225            };
226            let mut app_vals: Vec<PathBuf> = Vec::new();
227            for v in exp_files {
228                let path = v?;
229                if !recursive && source_path_is_dir(&path, !no_dereference) {
230                    return Err(ShellError::Generic(
231                        GenericError::new(
232                            "could_not_copy_directory",
233                            "resolves to a directory (not copied)",
234                            p.span,
235                        )
236                        .with_help("Directories must be copied using \"--recursive\""),
237                    ));
238                };
239                app_vals.push(path)
240            }
241            sources.push((app_vals, p.item.is_expand()));
242        }
243
244        // Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
245        // supported in Nushell
246        for (sources, need_expand_tilde) in sources.iter_mut() {
247            for src in sources.iter_mut() {
248                if !src.is_absolute() {
249                    *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
250                }
251            }
252        }
253        let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
254
255        let attributes = make_attributes(preserve)?;
256
257        let options = uu_cp::Options {
258            overwrite,
259            reflink_mode,
260            recursive,
261            debug,
262            attributes,
263            verbose: verbose || debug,
264            dereference: !recursive && !no_dereference,
265            progress_bar: progress,
266            attributes_only: false,
267            backup: BackupMode::None,
268            copy_contents: false,
269            cli_dereference: false,
270            copy_mode,
271            no_target_dir: false,
272            one_file_system: false,
273            parents: false,
274            sparse_mode: uu_cp::SparseMode::Auto,
275            strip_trailing_slashes: false,
276            backup_suffix: String::from("~"),
277            target_dir: None,
278            update,
279            set_selinux_context: false,
280            context: None,
281        };
282
283        if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
284            match error {
285                // code should still be EXIT_ERR as does GNU cp
286                CpError::NotAllFilesCopied => {}
287                _ => {
288                    return Err(ShellError::Generic(GenericError::new_internal(
289                        format!("{error}"),
290                        translate!(&error.to_string()),
291                    )));
292                }
293            };
294            // TODO: What should we do in place of set_exit_code?
295            // uucore::error::set_exit_code(EXIT_ERR);
296        }
297        Ok(PipelineData::empty())
298    }
299}
300
301const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true };
302const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true };
303
304fn make_attributes(preserve: Option<Value>) -> Result<uu_cp::Attributes, ShellError> {
305    if let Some(preserve) = preserve {
306        let mut attributes = uu_cp::Attributes {
307            #[cfg(any(
308                target_os = "linux",
309                target_os = "freebsd",
310                target_os = "android",
311                target_os = "macos",
312                target_os = "netbsd",
313                target_os = "openbsd"
314            ))]
315            ownership: ATTR_UNSET,
316            mode: ATTR_UNSET,
317            timestamps: ATTR_UNSET,
318            context: ATTR_UNSET,
319            links: ATTR_UNSET,
320            xattr: ATTR_UNSET,
321        };
322        parse_and_set_attributes_list(&preserve, &mut attributes)?;
323
324        Ok(attributes)
325    } else {
326        // By default don't preserve anything as per
327        // https://docs.rs/uu_cp/latest/uu_cp/struct.Attributes.html
328        Ok(uu_cp::Attributes::NONE)
329    }
330}
331
332fn source_path_is_dir(path: &Path, follow_symlink: bool) -> bool {
333    if follow_symlink {
334        return path.is_dir();
335    }
336
337    matches!(path.symlink_metadata(),
338        Ok(metadata) if metadata.file_type().is_dir()
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() -> nu_test_support::Result {
403        nu_test_support::test().examples(UCp)
404    }
405}