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