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::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 .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 file to a directory three levels above its current location.",
109 example: "cp myfile ....",
110 result: None,
111 },
112 ]
113 }
114
115 fn run(
116 &self,
117 engine_state: &EngineState,
118 stack: &mut Stack,
119 call: &Call,
120 _input: PipelineData,
121 ) -> Result<PipelineData, ShellError> {
122 let _ = localized_help_template("cp");
124
125 let interactive = call.has_flag(engine_state, stack, "interactive")?;
126 let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
127 (UpdateMode::IfOlder, CopyMode::Update)
128 } else {
129 (UpdateMode::All, CopyMode::Copy)
130 };
131
132 let force = call.has_flag(engine_state, stack, "force")?;
133 let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
134 let progress = call.has_flag(engine_state, stack, "progress")?;
135 let recursive = call.has_flag(engine_state, stack, "recursive")?;
136 let verbose = call.has_flag(engine_state, stack, "verbose")?;
137 let preserve: Option<Value> = call.get_flag(engine_state, stack, "preserve")?;
138 let all = call.has_flag(engine_state, stack, "all")?;
139
140 let debug = call.has_flag(engine_state, stack, "debug")?;
141 let overwrite = if no_clobber {
142 uu_cp::OverwriteMode::NoClobber
143 } else if interactive {
144 if force {
145 uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Force)
146 } else {
147 uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Standard)
148 }
149 } else if force {
150 uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Force)
151 } else {
152 uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Standard)
153 };
154 #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
155 let reflink_mode = uu_cp::ReflinkMode::Auto;
156 #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
157 let reflink_mode = uu_cp::ReflinkMode::Never;
158 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
159 if paths.is_empty() {
160 return Err(ShellError::Generic(
161 GenericError::new("Missing file operand", "Missing file operand", call.head)
162 .with_help("Please provide source and destination paths"),
163 ));
164 }
165
166 if paths.len() == 1 {
167 return Err(ShellError::Generic(GenericError::new(
168 "Missing destination path",
169 format!(
170 "Missing destination path operand after {}",
171 paths[0].item.as_ref()
172 ),
173 paths[0].span,
174 )));
175 }
176 let target = paths.pop().expect("Should not be reached?");
177 let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
178 target.item.to_string(),
179 ));
180 let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
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::Generic(GenericError::new(
184 "is not a directory",
185 "is not a directory",
186 target.span,
187 )));
188 };
189
190 let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
193 let glob_options = if all {
194 None
195 } else {
196 let glob_options = MatchOptions {
197 require_literal_leading_dot: true,
198 ..Default::default()
199 };
200 Some(glob_options)
201 };
202 for mut p in paths {
203 p.item = p.item.strip_ansi_string_unlikely();
204 let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
205 &p,
206 &cwd,
207 call.head,
208 glob_options,
209 engine_state.signals().clone(),
210 )
211 .map(|f| f.1)?
212 .collect();
213 if exp_files.is_empty() {
214 return Err(ShellError::Io(IoError::new(
215 shell_error::io::ErrorKind::FileNotFound,
216 p.span,
217 PathBuf::from(p.item.to_string()),
218 )));
219 };
220 let mut app_vals: Vec<PathBuf> = Vec::new();
221 for v in exp_files {
222 match v {
223 Ok(path) => {
224 if !recursive && path.is_dir() {
225 return Err(ShellError::Generic(
226 GenericError::new(
227 "could_not_copy_directory",
228 "resolves to a directory (not copied)",
229 p.span,
230 )
231 .with_help("Directories must be copied using \"--recursive\""),
232 ));
233 };
234 app_vals.push(path)
235 }
236 Err(e) => return Err(e),
237 }
238 }
239 sources.push((app_vals, p.item.is_expand()));
240 }
241
242 for (sources, need_expand_tilde) in sources.iter_mut() {
245 for src in sources.iter_mut() {
246 if !src.is_absolute() {
247 *src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
248 }
249 }
250 }
251 let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
252
253 let attributes = make_attributes(preserve)?;
254
255 let options = uu_cp::Options {
256 overwrite,
257 reflink_mode,
258 recursive,
259 debug,
260 attributes,
261 verbose: verbose || debug,
262 dereference: !recursive,
263 progress_bar: progress,
264 attributes_only: false,
265 backup: BackupMode::None,
266 copy_contents: false,
267 cli_dereference: false,
268 copy_mode,
269 no_target_dir: false,
270 one_file_system: false,
271 parents: false,
272 sparse_mode: uu_cp::SparseMode::Auto,
273 strip_trailing_slashes: false,
274 backup_suffix: String::from("~"),
275 target_dir: None,
276 update,
277 set_selinux_context: false,
278 context: None,
279 };
280
281 if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
282 match error {
283 CpError::NotAllFilesCopied => {}
285 _ => {
286 return Err(ShellError::Generic(GenericError::new_internal(
287 format!("{error}"),
288 translate!(&error.to_string()),
289 )));
290 }
291 };
292 }
295 Ok(PipelineData::empty())
296 }
297}
298
299const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true };
300const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true };
301
302fn make_attributes(preserve: Option<Value>) -> Result<uu_cp::Attributes, ShellError> {
303 if let Some(preserve) = preserve {
304 let mut attributes = uu_cp::Attributes {
305 #[cfg(any(
306 target_os = "linux",
307 target_os = "freebsd",
308 target_os = "android",
309 target_os = "macos",
310 target_os = "netbsd",
311 target_os = "openbsd"
312 ))]
313 ownership: ATTR_UNSET,
314 mode: ATTR_UNSET,
315 timestamps: ATTR_UNSET,
316 context: ATTR_UNSET,
317 links: ATTR_UNSET,
318 xattr: ATTR_UNSET,
319 };
320 parse_and_set_attributes_list(&preserve, &mut attributes)?;
321
322 Ok(attributes)
323 } else {
324 Ok(uu_cp::Attributes::NONE)
327 }
328}
329
330fn parse_and_set_attributes_list(
331 list: &Value,
332 attribute: &mut uu_cp::Attributes,
333) -> Result<(), ShellError> {
334 match list {
335 Value::List { vals, .. } => {
336 for val in vals {
337 parse_and_set_attribute(val, attribute)?;
338 }
339 Ok(())
340 }
341 _ => Err(ShellError::IncompatibleParametersSingle {
342 msg: "--preserve flag expects a list of strings".into(),
343 span: list.span(),
344 }),
345 }
346}
347
348fn parse_and_set_attribute(
349 value: &Value,
350 attribute: &mut uu_cp::Attributes,
351) -> Result<(), ShellError> {
352 match value {
353 Value::String { val, .. } => {
354 let attribute = match val.as_str() {
355 "mode" => &mut attribute.mode,
356 #[cfg(any(
357 target_os = "linux",
358 target_os = "freebsd",
359 target_os = "android",
360 target_os = "macos",
361 target_os = "netbsd",
362 target_os = "openbsd"
363 ))]
364 "ownership" => &mut attribute.ownership,
365 "timestamps" => &mut attribute.timestamps,
366 "context" => &mut attribute.context,
367 "link" | "links" => &mut attribute.links,
368 "xattr" => &mut attribute.xattr,
369 _ => {
370 return Err(ShellError::IncompatibleParametersSingle {
371 msg: format!("--preserve flag got an unexpected attribute \"{val}\""),
372 span: value.span(),
373 });
374 }
375 };
376 *attribute = ATTR_SET;
377 Ok(())
378 }
379 _ => Err(ShellError::IncompatibleParametersSingle {
380 msg: "--preserve flag expects a list of strings".into(),
381 span: value.span(),
382 }),
383 }
384}
385
386#[cfg(test)]
387mod test {
388 use super::*;
389 #[test]
390 fn test_examples() -> nu_test_support::Result {
391 nu_test_support::test().examples(UCp)
392 }
393}