1use nu_engine::command_prelude::*;
2use nu_glob::MatchOptions;
3use nu_protocol::{
4 NuGlob,
5 shell_error::{self, io::IoError},
6};
7use std::path::PathBuf;
8use uu_cp::{BackupMode, CopyMode, CpError, UpdateMode};
9use uucore::{localized_help_template, translate};
10
11#[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", "move 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 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::GenericError {
161 error: "Missing file operand".into(),
162 msg: "Missing file operand".into(),
163 span: Some(call.head),
164 help: Some("Please provide source and destination paths".into()),
165 inner: vec![],
166 });
167 }
168
169 if paths.len() == 1 {
170 return Err(ShellError::GenericError {
171 error: "Missing destination path".into(),
172 msg: format!(
173 "Missing destination path operand after {}",
174 paths[0].item.as_ref()
175 ),
176 span: Some(paths[0].span),
177 help: None,
178 inner: vec![],
179 });
180 }
181 let target = paths.pop().expect("Should not be reached?");
182 let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
183 target.item.to_string(),
184 ));
185 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
186 let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
187 if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() {
188 return Err(ShellError::GenericError {
189 error: "is not a directory".into(),
190 msg: "is not a directory".into(),
191 span: Some(target.span),
192 help: None,
193 inner: vec![],
194 });
195 };
196
197 let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
200 let glob_options = if all {
201 None
202 } else {
203 let glob_options = MatchOptions {
204 require_literal_leading_dot: true,
205 ..Default::default()
206 };
207 Some(glob_options)
208 };
209 for mut p in paths {
210 p.item = p.item.strip_ansi_string_unlikely();
211 let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
212 &p,
213 &cwd,
214 call.head,
215 glob_options,
216 engine_state.signals().clone(),
217 )
218 .map(|f| f.1)?
219 .collect();
220 if exp_files.is_empty() {
221 return Err(ShellError::Io(IoError::new(
222 shell_error::io::ErrorKind::FileNotFound,
223 p.span,
224 PathBuf::from(p.item.to_string()),
225 )));
226 };
227 let mut app_vals: Vec<PathBuf> = Vec::new();
228 for v in exp_files {
229 match v {
230 Ok(path) => {
231 if !recursive && path.is_dir() {
232 return Err(ShellError::GenericError {
233 error: "could_not_copy_directory".into(),
234 msg: "resolves to a directory (not copied)".into(),
235 span: Some(p.span),
236 help: Some(
237 "Directories must be copied using \"--recursive\"".into(),
238 ),
239 inner: vec![],
240 });
241 };
242 app_vals.push(path)
243 }
244 Err(e) => return Err(e),
245 }
246 }
247 sources.push((app_vals, p.item.is_expand()));
248 }
249
250 for (sources, need_expand_tilde) in sources.iter_mut() {
253 for src in sources.iter_mut() {
254 if !src.is_absolute() {
255 *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
256 }
257 }
258 }
259 let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
260
261 let attributes = make_attributes(preserve)?;
262
263 let options = uu_cp::Options {
264 overwrite,
265 reflink_mode,
266 recursive,
267 debug,
268 attributes,
269 verbose: verbose || debug,
270 dereference: !recursive,
271 progress_bar: progress,
272 attributes_only: false,
273 backup: BackupMode::None,
274 copy_contents: false,
275 cli_dereference: false,
276 copy_mode,
277 no_target_dir: false,
278 one_file_system: false,
279 parents: false,
280 sparse_mode: uu_cp::SparseMode::Auto,
281 strip_trailing_slashes: false,
282 backup_suffix: String::from("~"),
283 target_dir: None,
284 update,
285 set_selinux_context: false,
286 context: None,
287 };
288
289 if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
290 match error {
291 CpError::NotAllFilesCopied => {}
293 _ => {
294 eprintln!("here");
295 return Err(ShellError::GenericError {
296 error: format!("{error}"),
297 msg: translate!(&error.to_string()),
298 span: None,
299 help: None,
300 inner: vec![],
301 });
302 }
303 };
304 }
307 Ok(PipelineData::empty())
308 }
309}
310
311const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true };
312const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true };
313
314fn make_attributes(preserve: Option<Value>) -> Result<uu_cp::Attributes, ShellError> {
315 if let Some(preserve) = preserve {
316 let mut attributes = uu_cp::Attributes {
317 #[cfg(any(
318 target_os = "linux",
319 target_os = "freebsd",
320 target_os = "android",
321 target_os = "macos",
322 target_os = "netbsd",
323 target_os = "openbsd"
324 ))]
325 ownership: ATTR_UNSET,
326 mode: ATTR_UNSET,
327 timestamps: ATTR_UNSET,
328 context: ATTR_UNSET,
329 links: ATTR_UNSET,
330 xattr: ATTR_UNSET,
331 };
332 parse_and_set_attributes_list(&preserve, &mut attributes)?;
333
334 Ok(attributes)
335 } else {
336 Ok(uu_cp::Attributes {
338 mode: ATTR_SET,
339 #[cfg(any(
340 target_os = "linux",
341 target_os = "freebsd",
342 target_os = "android",
343 target_os = "macos",
344 target_os = "netbsd",
345 target_os = "openbsd"
346 ))]
347 ownership: ATTR_UNSET,
348 timestamps: ATTR_UNSET,
349 context: ATTR_UNSET,
350 links: ATTR_UNSET,
351 xattr: ATTR_UNSET,
352 })
353 }
354}
355
356fn parse_and_set_attributes_list(
357 list: &Value,
358 attribute: &mut uu_cp::Attributes,
359) -> Result<(), ShellError> {
360 match list {
361 Value::List { vals, .. } => {
362 for val in vals {
363 parse_and_set_attribute(val, attribute)?;
364 }
365 Ok(())
366 }
367 _ => Err(ShellError::IncompatibleParametersSingle {
368 msg: "--preserve flag expects a list of strings".into(),
369 span: list.span(),
370 }),
371 }
372}
373
374fn parse_and_set_attribute(
375 value: &Value,
376 attribute: &mut uu_cp::Attributes,
377) -> Result<(), ShellError> {
378 match value {
379 Value::String { val, .. } => {
380 let attribute = match val.as_str() {
381 "mode" => &mut attribute.mode,
382 #[cfg(any(
383 target_os = "linux",
384 target_os = "freebsd",
385 target_os = "android",
386 target_os = "macos",
387 target_os = "netbsd",
388 target_os = "openbsd"
389 ))]
390 "ownership" => &mut attribute.ownership,
391 "timestamps" => &mut attribute.timestamps,
392 "context" => &mut attribute.context,
393 "link" | "links" => &mut attribute.links,
394 "xattr" => &mut attribute.xattr,
395 _ => {
396 return Err(ShellError::IncompatibleParametersSingle {
397 msg: format!("--preserve flag got an unexpected attribute \"{val}\""),
398 span: value.span(),
399 });
400 }
401 };
402 *attribute = ATTR_SET;
403 Ok(())
404 }
405 _ => Err(ShellError::IncompatibleParametersSingle {
406 msg: "--preserve flag expects a list of strings".into(),
407 span: value.span(),
408 }),
409 }
410}
411
412#[cfg(test)]
413mod test {
414 use super::*;
415 #[test]
416 fn test_examples() {
417 use crate::test_examples;
418
419 test_examples(UCp {})
420 }
421}