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