Files
nushell/crates/nu-command/src/filesystem/touch.rs
Devyn Cairns 51aa66fef7 Fix #12391: mkdir uses process startup directory instead of current script directory (#12394)
# Description

This fixes #12391.

nushell/nushell@87c5f6e455 accidentally introduced a bug where the path
was not being properly
expanded according to the cwd. This makes both 'touch' and 'mkdir' use
globs just like the rest of
the commands to preserve tilde behavior while still expanding the paths
properly.

This doesn't actually expand the globs. Should it?

# User-Facing Changes

- Restore behavior of `mkdir`, `touch`
- Help text now says they can take globs, but they won't actually expand
them, maybe this should be changed

# Tests + Formatting

Regression tests added.


# After Submitting

This is severe enough and should be included in the point release.
2024-04-04 14:23:10 +02:00

209 lines
7.4 KiB
Rust

use filetime::FileTime;
use nu_engine::{command_prelude::*, current_dir};
use nu_path::expand_path_with;
use nu_protocol::NuGlob;
use std::{fs::OpenOptions, path::Path, time::SystemTime};
use super::util::get_rest_for_glob_pattern;
#[derive(Clone)]
pub struct Touch;
impl Command for Touch {
fn name(&self) -> &str {
"touch"
}
fn search_terms(&self) -> Vec<&str> {
vec!["create", "file"]
}
fn signature(&self) -> Signature {
Signature::build("touch")
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.rest(
"files",
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Filepath]),
"The file(s) to create."
)
.named(
"reference",
SyntaxShape::String,
"change the file or directory time to the time of the reference file/directory",
Some('r'),
)
.switch(
"modified",
"change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
Some('m'),
)
.switch(
"access",
"change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
Some('a'),
)
.switch(
"no-create",
"do not create the file if it does not exist",
Some('c'),
)
.category(Category::FileSystem)
}
fn usage(&self) -> &str {
"Creates one or more files."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
let mut change_atime: bool = call.has_flag(engine_state, stack, "access")?;
let reference: Option<Spanned<String>> = call.get_flag(engine_state, stack, "reference")?;
let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
let files: Vec<Spanned<NuGlob>> = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
if files.is_empty() {
return Err(ShellError::MissingParameter {
param_name: "requires file paths".to_string(),
span: call.head,
});
}
let mut mtime = SystemTime::now();
let mut atime = mtime;
// Change both times if neither is specified
if !change_mtime && !change_atime {
change_mtime = true;
change_atime = true;
}
if let Some(reference) = reference {
let reference_path = Path::new(&reference.item);
if !reference_path.exists() {
return Err(ShellError::FileNotFoundCustom {
msg: "Reference path not found".into(),
span: reference.span,
});
}
let metadata = reference_path
.metadata()
.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("Failed to read metadata: {err}"),
span: reference.span,
})?;
mtime = metadata
.modified()
.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("Failed to read modified time: {err}"),
span: reference.span,
})?;
atime = metadata
.accessed()
.map_err(|err| ShellError::IOErrorSpanned {
msg: format!("Failed to read access time: {err}"),
span: reference.span,
})?;
}
let cwd = current_dir(engine_state, stack)?;
for (index, glob) in files.into_iter().enumerate() {
let path = expand_path_with(glob.item.as_ref(), &cwd, glob.item.is_expand());
// If --no-create is passed and the file/dir does not exist there's nothing to do
if no_create && !path.exists() {
continue;
}
// Create a file at the given path unless the path is a directory
if !path.is_dir() {
if let Err(err) = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&path)
{
return Err(ShellError::CreateNotPossible {
msg: format!("Failed to create file: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
});
};
}
if change_mtime {
if let Err(err) = filetime::set_file_mtime(&path, FileTime::from_system_time(mtime))
{
return Err(ShellError::ChangeModifiedTimeNotPossible {
msg: format!("Failed to change the modified time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
});
};
}
if change_atime {
if let Err(err) = filetime::set_file_atime(&path, FileTime::from_system_time(atime))
{
return Err(ShellError::ChangeAccessTimeNotPossible {
msg: format!("Failed to change the access time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
});
};
}
}
Ok(PipelineData::empty())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Creates \"fixture.json\"",
example: "touch fixture.json",
result: None,
},
Example {
description: "Creates files a, b and c",
example: "touch a b c",
result: None,
},
Example {
description: r#"Changes the last modified time of "fixture.json" to today's date"#,
example: "touch -m fixture.json",
result: None,
},
Example {
description: "Changes the last modified time of files a, b and c to a date",
example: r#"touch -m -d "yesterday" a b c"#,
result: None,
},
Example {
description: r#"Changes the last modified time of file d and e to "fixture.json"'s last modified time"#,
example: r#"touch -m -r fixture.json d e"#,
result: None,
},
Example {
description: r#"Changes the last accessed time of "fixture.json" to a date"#,
example: r#"touch -a -d "August 24, 2019; 12:30:30" fixture.json"#,
result: None,
},
]
}
}