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::{MAIN_SEPARATOR, Path, PathBuf};
8use uu_cp::{BackupMode, CopyMode, CpError, UpdateMode};
9use uucore::{localized_help_template, translate};
10
11#[derive(Clone)]
15pub struct UCp;
16
17impl Command for UCp {
18 fn name(&self) -> &str {
19 "cp"
20 }
21
22 fn description(&self) -> &str {
23 "Copy files using uutils/coreutils cp."
24 }
25
26 fn search_terms(&self) -> Vec<&str> {
27 vec!["copy", "file", "files", "coreutils"]
28 }
29
30 fn signature(&self) -> Signature {
31 Signature::build("cp")
32 .input_output_types(vec![(Type::Nothing, Type::Nothing)])
33 .switch("recursive", "Copy directories recursively.", Some('r'))
34 .switch(
35 "no-dereference",
36 "Copy symbolic links as symbolic links instead of their targets.",
37 Some('P'),
38 )
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 a symbolic link without copying the target.",
109 example: "cp --no-dereference link-to-file newlink",
110 result: None,
111 },
112 Example {
113 description: "Copy file to a directory three levels above its current location.",
114 example: "cp myfile ....",
115 result: None,
116 },
117 ]
118 }
119
120 fn run(
121 &self,
122 engine_state: &EngineState,
123 stack: &mut Stack,
124 call: &Call,
125 _input: PipelineData,
126 ) -> Result<PipelineData, ShellError> {
127 let _ = localized_help_template("cp");
129
130 let interactive = call.has_flag(engine_state, stack, "interactive")?;
131 let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
132 (UpdateMode::IfOlder, CopyMode::Update)
133 } else {
134 (UpdateMode::All, CopyMode::Copy)
135 };
136
137 let force = call.has_flag(engine_state, stack, "force")?;
138 let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
139 let progress = call.has_flag(engine_state, stack, "progress")?;
140 let recursive = call.has_flag(engine_state, stack, "recursive")?;
141 let no_dereference = call.has_flag(engine_state, stack, "no-dereference")?;
142 let verbose = call.has_flag(engine_state, stack, "verbose")?;
143 let preserve: Option<Value> = call.get_flag(engine_state, stack, "preserve")?;
144 let all = call.has_flag(engine_state, stack, "all")?;
145
146 let debug = call.has_flag(engine_state, stack, "debug")?;
147 let overwrite = if no_clobber {
148 uu_cp::OverwriteMode::NoClobber
149 } else if interactive {
150 if force {
151 uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Force)
152 } else {
153 uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Standard)
154 }
155 } else if force {
156 uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Force)
157 } else {
158 uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Standard)
159 };
160 #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
161 let reflink_mode = uu_cp::ReflinkMode::Auto;
162 #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
163 let reflink_mode = uu_cp::ReflinkMode::Never;
164 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
165 if paths.is_empty() {
166 return Err(ShellError::Generic(
167 GenericError::new("Missing file operand", "Missing file operand", call.head)
168 .with_help("Please provide source and destination paths"),
169 ));
170 }
171
172 if paths.len() == 1 {
173 return Err(ShellError::Generic(GenericError::new(
174 "Missing destination path",
175 format!(
176 "Missing destination path operand after {}",
177 paths[0].item.as_ref()
178 ),
179 paths[0].span,
180 )));
181 }
182 let target = paths.pop().expect("Should not be reached?");
183 let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
184 target.item.to_string(),
185 ));
186 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
187 let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
188 if target.item.as_ref().ends_with(MAIN_SEPARATOR) && !target_path.is_dir() {
189 return Err(ShellError::Generic(GenericError::new(
190 "is not a directory",
191 "is not a directory",
192 target.span,
193 )));
194 };
195
196 let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
199 let glob_options = if all {
200 None
201 } else {
202 let glob_options = MatchOptions {
203 require_literal_leading_dot: true,
204 ..Default::default()
205 };
206 Some(glob_options)
207 };
208 for mut p in paths {
209 p.item = p.item.strip_ansi_string_unlikely();
210 let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
211 &p,
212 &cwd,
213 call.head,
214 glob_options,
215 engine_state.signals().clone(),
216 )
217 .map(|f| f.1)?
218 .collect();
219 if exp_files.is_empty() {
220 return Err(ShellError::Io(IoError::new(
221 shell_error::io::ErrorKind::FileNotFound,
222 p.span,
223 PathBuf::from(p.item.to_string()),
224 )));
225 };
226 let mut app_vals: Vec<PathBuf> = Vec::new();
227 for v in exp_files {
228 let path = v?;
229 if !recursive && source_path_is_dir(&path, !no_dereference) {
230 return Err(ShellError::Generic(
231 GenericError::new(
232 "could_not_copy_directory",
233 "resolves to a directory (not copied)",
234 p.span,
235 )
236 .with_help("Directories must be copied using \"--recursive\""),
237 ));
238 };
239 app_vals.push(path)
240 }
241 sources.push((app_vals, p.item.is_expand()));
242 }
243
244 for (sources, need_expand_tilde) in sources.iter_mut() {
247 for src in sources.iter_mut() {
248 if !src.is_absolute() {
249 *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
250 }
251 }
252 }
253 let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
254
255 let attributes = make_attributes(preserve)?;
256
257 let options = uu_cp::Options {
258 overwrite,
259 reflink_mode,
260 recursive,
261 debug,
262 attributes,
263 verbose: verbose || debug,
264 dereference: !recursive && !no_dereference,
265 progress_bar: progress,
266 attributes_only: false,
267 backup: BackupMode::None,
268 copy_contents: false,
269 cli_dereference: false,
270 copy_mode,
271 no_target_dir: false,
272 one_file_system: false,
273 parents: false,
274 sparse_mode: uu_cp::SparseMode::Auto,
275 strip_trailing_slashes: false,
276 backup_suffix: String::from("~"),
277 target_dir: None,
278 update,
279 set_selinux_context: false,
280 context: None,
281 };
282
283 if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
284 match error {
285 CpError::NotAllFilesCopied => {}
287 _ => {
288 return Err(ShellError::Generic(GenericError::new_internal(
289 format!("{error}"),
290 translate!(&error.to_string()),
291 )));
292 }
293 };
294 }
297 Ok(PipelineData::empty())
298 }
299}
300
301const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true };
302const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true };
303
304fn make_attributes(preserve: Option<Value>) -> Result<uu_cp::Attributes, ShellError> {
305 if let Some(preserve) = preserve {
306 let mut attributes = uu_cp::Attributes {
307 #[cfg(any(
308 target_os = "linux",
309 target_os = "freebsd",
310 target_os = "android",
311 target_os = "macos",
312 target_os = "netbsd",
313 target_os = "openbsd"
314 ))]
315 ownership: ATTR_UNSET,
316 mode: ATTR_UNSET,
317 timestamps: ATTR_UNSET,
318 context: ATTR_UNSET,
319 links: ATTR_UNSET,
320 xattr: ATTR_UNSET,
321 };
322 parse_and_set_attributes_list(&preserve, &mut attributes)?;
323
324 Ok(attributes)
325 } else {
326 Ok(uu_cp::Attributes::NONE)
329 }
330}
331
332fn source_path_is_dir(path: &Path, follow_symlink: bool) -> bool {
333 if follow_symlink {
334 return path.is_dir();
335 }
336
337 matches!(path.symlink_metadata(),
338 Ok(metadata) if metadata.file_type().is_dir()
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() -> nu_test_support::Result {
403 nu_test_support::test().examples(UCp)
404 }
405}