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, 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 .rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "Copy SRC file/s to DEST.")
65 .allow_variants_without_examples(true)
66 .category(Category::FileSystem)
67 }
68
69 fn examples(&self) -> Vec<Example<'_>> {
70 vec![
71 Example {
72 description: "Copy myfile to dir_b",
73 example: "cp myfile dir_b",
74 result: None,
75 },
76 Example {
77 description: "Recursively copy dir_a to dir_b",
78 example: "cp -r dir_a dir_b",
79 result: None,
80 },
81 Example {
82 description: "Recursively copy dir_a to dir_b, and print the feedbacks",
83 example: "cp -r -v dir_a dir_b",
84 result: None,
85 },
86 Example {
87 description: "Move many files into a directory",
88 example: "cp *.txt dir_a",
89 result: None,
90 },
91 Example {
92 description: "Copy only if source file is newer than target file",
93 example: "cp -u myfile newfile",
94 result: None,
95 },
96 Example {
97 description: "Copy file preserving mode and timestamps attributes",
98 example: "cp --preserve [ mode timestamps ] myfile newfile",
99 result: None,
100 },
101 Example {
102 description: "Copy file erasing all attributes",
103 example: "cp --preserve [] myfile newfile",
104 result: None,
105 },
106 Example {
107 description: "Copy file to a directory three levels above its current location",
108 example: "cp myfile ....",
109 result: None,
110 },
111 ]
112 }
113
114 fn run(
115 &self,
116 engine_state: &EngineState,
117 stack: &mut Stack,
118 call: &Call,
119 _input: PipelineData,
120 ) -> Result<PipelineData, ShellError> {
121 let _ = localized_help_template("cp");
123
124 let interactive = call.has_flag(engine_state, stack, "interactive")?;
125 let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
126 (UpdateMode::IfOlder, CopyMode::Update)
127 } else {
128 (UpdateMode::All, CopyMode::Copy)
129 };
130
131 let force = call.has_flag(engine_state, stack, "force")?;
132 let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
133 let progress = call.has_flag(engine_state, stack, "progress")?;
134 let recursive = call.has_flag(engine_state, stack, "recursive")?;
135 let verbose = call.has_flag(engine_state, stack, "verbose")?;
136 let preserve: Option<Value> = call.get_flag(engine_state, stack, "preserve")?;
137
138 let debug = call.has_flag(engine_state, stack, "debug")?;
139 let overwrite = if no_clobber {
140 uu_cp::OverwriteMode::NoClobber
141 } else if interactive {
142 if force {
143 uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Force)
144 } else {
145 uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Standard)
146 }
147 } else if force {
148 uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Force)
149 } else {
150 uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Standard)
151 };
152 #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
153 let reflink_mode = uu_cp::ReflinkMode::Auto;
154 #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
155 let reflink_mode = uu_cp::ReflinkMode::Never;
156 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
157 if paths.is_empty() {
158 return Err(ShellError::GenericError {
159 error: "Missing file operand".into(),
160 msg: "Missing file operand".into(),
161 span: Some(call.head),
162 help: Some("Please provide source and destination paths".into()),
163 inner: vec![],
164 });
165 }
166
167 if paths.len() == 1 {
168 return Err(ShellError::GenericError {
169 error: "Missing destination path".into(),
170 msg: format!(
171 "Missing destination path operand after {}",
172 paths[0].item.as_ref()
173 ),
174 span: Some(paths[0].span),
175 help: None,
176 inner: vec![],
177 });
178 }
179 let target = paths.pop().expect("Should not be reached?");
180 let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
181 target.item.to_string(),
182 ));
183 #[allow(deprecated)]
184 let cwd = current_dir(engine_state, stack)?;
185 let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
186 if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() {
187 return Err(ShellError::GenericError {
188 error: "is not a directory".into(),
189 msg: "is not a directory".into(),
190 span: Some(target.span),
191 help: None,
192 inner: vec![],
193 });
194 };
195
196 let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
199
200 for mut p in paths {
201 p.item = p.item.strip_ansi_string_unlikely();
202 let exp_files: Vec<Result<PathBuf, ShellError>> =
203 nu_engine::glob_from(&p, &cwd, call.head, None, engine_state.signals().clone())
204 .map(|f| f.1)?
205 .collect();
206 if exp_files.is_empty() {
207 return Err(ShellError::Io(IoError::new(
208 shell_error::io::ErrorKind::FileNotFound,
209 p.span,
210 PathBuf::from(p.item.to_string()),
211 )));
212 };
213 let mut app_vals: Vec<PathBuf> = Vec::new();
214 for v in exp_files {
215 match v {
216 Ok(path) => {
217 if !recursive && path.is_dir() {
218 return Err(ShellError::GenericError {
219 error: "could_not_copy_directory".into(),
220 msg: "resolves to a directory (not copied)".into(),
221 span: Some(p.span),
222 help: Some(
223 "Directories must be copied using \"--recursive\"".into(),
224 ),
225 inner: vec![],
226 });
227 };
228 app_vals.push(path)
229 }
230 Err(e) => return Err(e),
231 }
232 }
233 sources.push((app_vals, p.item.is_expand()));
234 }
235
236 for (sources, need_expand_tilde) in sources.iter_mut() {
239 for src in sources.iter_mut() {
240 if !src.is_absolute() {
241 *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
242 }
243 }
244 }
245 let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
246
247 let attributes = make_attributes(preserve)?;
248
249 let options = uu_cp::Options {
250 overwrite,
251 reflink_mode,
252 recursive,
253 debug,
254 attributes,
255 verbose: verbose || debug,
256 dereference: !recursive,
257 progress_bar: progress,
258 attributes_only: false,
259 backup: BackupMode::None,
260 copy_contents: false,
261 cli_dereference: false,
262 copy_mode,
263 no_target_dir: false,
264 one_file_system: false,
265 parents: false,
266 sparse_mode: uu_cp::SparseMode::Auto,
267 strip_trailing_slashes: false,
268 backup_suffix: String::from("~"),
269 target_dir: None,
270 update,
271 set_selinux_context: false,
272 context: None,
273 };
274
275 if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
276 match error {
277 CpError::NotAllFilesCopied => {}
279 _ => {
280 eprintln!("here");
281 return Err(ShellError::GenericError {
282 error: format!("{error}"),
283 msg: translate!(&error.to_string()),
284 span: None,
285 help: None,
286 inner: vec![],
287 });
288 }
289 };
290 }
293 Ok(PipelineData::empty())
294 }
295}
296
297const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true };
298const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true };
299
300fn make_attributes(preserve: Option<Value>) -> Result<uu_cp::Attributes, ShellError> {
301 if let Some(preserve) = preserve {
302 let mut attributes = uu_cp::Attributes {
303 #[cfg(any(
304 target_os = "linux",
305 target_os = "freebsd",
306 target_os = "android",
307 target_os = "macos",
308 target_os = "netbsd",
309 target_os = "openbsd"
310 ))]
311 ownership: ATTR_UNSET,
312 mode: ATTR_UNSET,
313 timestamps: ATTR_UNSET,
314 context: ATTR_UNSET,
315 links: ATTR_UNSET,
316 xattr: ATTR_UNSET,
317 };
318 parse_and_set_attributes_list(&preserve, &mut attributes)?;
319
320 Ok(attributes)
321 } else {
322 Ok(uu_cp::Attributes {
324 mode: ATTR_SET,
325 #[cfg(any(
326 target_os = "linux",
327 target_os = "freebsd",
328 target_os = "android",
329 target_os = "macos",
330 target_os = "netbsd",
331 target_os = "openbsd"
332 ))]
333 ownership: ATTR_UNSET,
334 timestamps: ATTR_UNSET,
335 context: ATTR_UNSET,
336 links: ATTR_UNSET,
337 xattr: ATTR_UNSET,
338 })
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() {
403 use crate::test_examples;
404
405 test_examples(UCp {})
406 }
407}