Skip to main content

nu_command/filesystem/
utouch.rs

1use chrono::{DateTime, FixedOffset};
2use filetime::FileTime;
3use nu_engine::command_prelude::*;
4use nu_path::expand_path_with;
5use nu_protocol::{
6    NuGlob, shell_error::generic::GenericError, shell_error::io::ErrorKind,
7    shell_error::io::IoError,
8};
9use std::path::PathBuf;
10use uu_touch::{ChangeTimes, InputFile, Options, Source, error::TouchError};
11use uucore::{localized_help_template, translate};
12
13#[derive(Clone)]
14pub struct UTouch;
15
16impl Command for UTouch {
17    fn name(&self) -> &str {
18        "touch"
19    }
20
21    fn search_terms(&self) -> Vec<&str> {
22        vec!["create", "file", "coreutils"]
23    }
24
25    fn signature(&self) -> Signature {
26        Signature::build("touch")
27            .input_output_types(vec![ (Type::Nothing, Type::Nothing) ])
28            .rest(
29                "files",
30                SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]),
31                "The file(s) to create. '-' is used to represent stdout."
32            )
33            .named(
34                "reference",
35                SyntaxShape::Filepath,
36                "Use the access and modification times of the reference file/directory instead of the current time.",
37                Some('r'),
38            )
39            .named(
40                "timestamp",
41                SyntaxShape::DateTime,
42                "Use the given timestamp instead of the current time.",
43                Some('t')
44            )
45            .named(
46                "date",
47                SyntaxShape::String,
48                "Use the given time instead of the current time. This can be a full timestamp or it can be relative to either the current time or reference file time (if given). For more information, see https://www.gnu.org/software/coreutils/manual/html_node/touch-invocation.html.",
49                Some('d')
50            )
51            .switch(
52                "modified",
53                "Change only the modification time (if used with -a, access time is changed too).",
54                Some('m'),
55            )
56            .switch(
57                "access",
58                "Change only the access time (if used with -m, modification time is changed too).",
59                Some('a'),
60            )
61            .switch(
62                "no-create",
63                "Don't create the file if it doesn't exist.",
64                Some('c'),
65            )
66            .switch(
67                "no-deref",
68                "Affect each symbolic link instead of any referenced file (only for systems that can change the timestamps of a symlink). Ignored if touching stdout.",
69                Some('s'),
70            )
71            .category(Category::FileSystem)
72    }
73
74    fn description(&self) -> &str {
75        "Creates one or more files."
76    }
77
78    fn run(
79        &self,
80        engine_state: &EngineState,
81        stack: &mut Stack,
82        call: &Call,
83        _input: PipelineData,
84    ) -> Result<PipelineData, ShellError> {
85        // setup the uutils error translation
86        let _ = localized_help_template("touch");
87
88        let change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
89        let change_atime: bool = call.has_flag(engine_state, stack, "access")?;
90        let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
91        let no_deref: bool = call.has_flag(engine_state, stack, "no-deref")?;
92        let file_globs = call
93            .rest::<Spanned<NuGlob>>(engine_state, stack, 0)
94            .map_err(|err| match err {
95                ShellError::CantConvert { span, .. } => ShellError::IncompatibleParametersSingle {
96                    msg: "requires file paths".to_string(),
97                    span,
98                },
99                _ => err,
100            })?;
101        let cwd = engine_state.cwd(Some(stack))?;
102
103        if file_globs.is_empty() {
104            return Err(ShellError::MissingParameter {
105                param_name: "requires file paths".to_string(),
106                span: call.head,
107            });
108        }
109
110        let (reference_file, reference_span) = if let Some(reference) =
111            call.get_flag::<Spanned<PathBuf>>(engine_state, stack, "reference")?
112        {
113            (Some(reference.item), Some(reference.span))
114        } else {
115            (None, None)
116        };
117        let (date_str, date_span) =
118            if let Some(date) = call.get_flag::<Spanned<String>>(engine_state, stack, "date")? {
119                (Some(date.item), Some(date.span))
120            } else {
121                (None, None)
122            };
123        let timestamp: Option<Spanned<DateTime<FixedOffset>>> =
124            call.get_flag(engine_state, stack, "timestamp")?;
125
126        let source = if let Some(timestamp) = timestamp {
127            if let Some(reference_span) = reference_span {
128                return Err(ShellError::IncompatibleParameters {
129                    left_message: "timestamp given".to_string(),
130                    left_span: timestamp.span,
131                    right_message: "reference given".to_string(),
132                    right_span: reference_span,
133                });
134            }
135            if let Some(date_span) = date_span {
136                return Err(ShellError::IncompatibleParameters {
137                    left_message: "timestamp given".to_string(),
138                    left_span: timestamp.span,
139                    right_message: "date given".to_string(),
140                    right_span: date_span,
141                });
142            }
143            Source::Timestamp(FileTime::from_unix_time(
144                timestamp.item.timestamp(),
145                timestamp.item.timestamp_subsec_nanos(),
146            ))
147        } else if let Some(reference_file) = reference_file {
148            let reference_file = expand_path_with(reference_file, &cwd, true);
149            Source::Reference(reference_file)
150        } else {
151            Source::Now
152        };
153
154        let change_times = if change_atime && !change_mtime {
155            ChangeTimes::AtimeOnly
156        } else if change_mtime && !change_atime {
157            ChangeTimes::MtimeOnly
158        } else {
159            ChangeTimes::Both
160        };
161
162        let mut input_files = Vec::new();
163        for file_glob in &file_globs {
164            if file_glob.item.as_ref() == "-" {
165                input_files.push(InputFile::Stdout);
166            } else {
167                let file_path =
168                    expand_path_with(file_glob.item.as_ref(), &cwd, file_glob.item.is_expand());
169
170                if !file_glob.item.is_expand() {
171                    if no_create && !file_path.exists() {
172                        continue;
173                    }
174
175                    input_files.push(InputFile::Path(file_path));
176                    continue;
177                }
178
179                let expanded_globs = match nu_engine::glob_from(
180                    file_glob,
181                    cwd.as_ref(),
182                    file_glob.span,
183                    None,
184                    engine_state.signals().clone(),
185                ) {
186                    Ok((_, expanded_globs)) => expanded_globs,
187                    Err(err)
188                        if matches!(
189                            &err,
190                            ShellError::Io(IoError {
191                                kind: ErrorKind::Std(std::io::ErrorKind::NotFound, ..)
192                                    | ErrorKind::FileNotFound
193                                    | ErrorKind::DirectoryNotFound,
194                                ..
195                            })
196                        ) =>
197                    {
198                        let Some(file_name) = file_path.file_name() else {
199                            return Err(err);
200                        };
201
202                        if nu_glob::is_glob_with_backend(&file_name.to_string_lossy()) {
203                            return Err(err);
204                        }
205
206                        if no_create && !file_path.exists() {
207                            continue;
208                        }
209
210                        input_files.push(InputFile::Path(file_path));
211                        continue;
212                    }
213                    Err(err) => return Err(err),
214                };
215
216                let expanded_globs: Vec<PathBuf> = expanded_globs
217                    .filter_map(Result::ok)
218                    .map(|path| {
219                        if path.is_absolute() {
220                            path
221                        } else {
222                            cwd.as_std_path().join(path)
223                        }
224                    })
225                    .collect();
226
227                if expanded_globs.is_empty() {
228                    let Some(file_name) = file_path.file_name() else {
229                        return Err(ShellError::Generic(GenericError::new(
230                            format!(
231                                "Could not process file path {}",
232                                file_path.to_string_lossy()
233                            ),
234                            "invalid file path",
235                            file_glob.span,
236                        )));
237                    };
238
239                    if nu_glob::is_glob_with_backend(&file_name.to_string_lossy()) {
240                        return Err(ShellError::Generic(
241                            GenericError::new(
242                                format!(
243                                    "No matches found for glob {}",
244                                    file_name.to_string_lossy()
245                                ),
246                                "No matches found for glob",
247                                file_glob.span,
248                            )
249                            .with_help(format!(
250                                "Use quotes if you want to create a file named {}",
251                                file_name.to_string_lossy()
252                            )),
253                        ));
254                    }
255
256                    if no_create && !file_path.exists() {
257                        continue;
258                    }
259
260                    input_files.push(InputFile::Path(file_path));
261                    continue;
262                }
263
264                input_files.extend(expanded_globs.into_iter().map(InputFile::Path));
265            }
266        }
267
268        if let Err(err) = uu_touch::touch(
269            &input_files,
270            &Options {
271                no_create,
272                no_deref,
273                source,
274                date: date_str,
275                change_times,
276                strict: true,
277            },
278        ) {
279            let nu_err = match err {
280                TouchError::TouchFileError { path, index, error } => {
281                    ShellError::Generic(GenericError::new(
282                        format!("Could not touch {}", path.display()),
283                        translate!(&error.to_string()),
284                        file_globs[index].span,
285                    ))
286                }
287                TouchError::InvalidDateFormat(date) => ShellError::IncorrectValue {
288                    msg: format!("Invalid date: {date}"),
289                    val_span: date_span.expect("touch should've been given a date"),
290                    call_span: call.head,
291                },
292                TouchError::ReferenceFileInaccessible(reference_path, io_err) => {
293                    let span = reference_span.expect("touch should've been given a reference file");
294                    ShellError::Io(IoError::new_with_additional_context(
295                        io_err,
296                        span,
297                        reference_path,
298                        "failed to read metadata",
299                    ))
300                }
301                _ => ShellError::Generic(GenericError::new(
302                    format!("{err}"),
303                    translate!(&err.to_string()),
304                    call.head,
305                )),
306            };
307            return Err(nu_err);
308        }
309
310        Ok(PipelineData::empty())
311    }
312
313    fn examples(&self) -> Vec<Example<'_>> {
314        vec![
315            Example {
316                description: "Creates \"fixture.json\".",
317                example: "touch fixture.json",
318                result: None,
319            },
320            Example {
321                description: "Creates files a, b and c.",
322                example: "touch a b c",
323                result: None,
324            },
325            Example {
326                description: r#"Changes the last modified time of "fixture.json" to today's date."#,
327                example: "touch -m fixture.json",
328                result: None,
329            },
330            Example {
331                description: "Changes the last modified and accessed time of all files with the .json extension to today's date.",
332                example: "touch *.json",
333                result: None,
334            },
335            Example {
336                description: "Changes the last accessed and modified times of files a, b and c to the current time but yesterday.",
337                example: r#"touch -d "yesterday" a b c"#,
338                result: None,
339            },
340            Example {
341                description: r#"Changes the last modified time of files d and e to "fixture.json"'s last modified time."#,
342                example: "touch -m -r fixture.json d e",
343                result: None,
344            },
345            Example {
346                description: r#"Changes the last accessed time of "fixture.json" to a datetime."#,
347                example: "touch -a -t 2019-08-24T12:30:30 fixture.json",
348                result: None,
349            },
350            Example {
351                description: "Change the last accessed and modified times of stdout.",
352                example: "touch -",
353                result: None,
354            },
355            Example {
356                description: r#"Changes the last accessed and modified times of file a to 1 month before "fixture.json"'s last modified time."#,
357                example: r#"touch -r fixture.json -d "-1 month" a"#,
358                result: None,
359            },
360        ]
361    }
362}