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