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