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 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}