Files
nushell/crates/nu-command/src/filesystem/touch.rs
kik4444 38a42905ae Fix touch to allow changing timestamps on directories, remake from #11760 (#12005)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

Based off of #11760 to be mergable without conflicts.

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
Fix for #11757.
The main issue in #11757 is I tried to copy the timestamp from one
directory to another only to realize that did not work whereas the
coreutils `^touch` had no problems. I thought `--reference` just did not
work, but apparently the whole `touch` command could not work on
directories because
`OpenOptions::new().write(true).create(true).open(&item)` tries to
create `touch`'s target in advance and then modify its timestamps. But
if the target is a directory that already exists then this would fail
even though the crate used for working with timestamps, `filetime`,
already works on directories.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
I don't believe this should change any existing valid behaviors. It just
changes a non-working behavior.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
~~I only could not run `cargo test` because I get compilation errors on
the latest main branch~~
All tests pass with `cargo test --features=sqlite`

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2024-03-01 07:23:03 -06:00

206 lines
7.2 KiB
Rust

use std::path::Path;
use std::{fs::OpenOptions, time::SystemTime};
use filetime::FileTime;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type,
};
#[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::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<String> = call.rest(engine_state, stack, 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,
})?;
}
for (index, item) in files.into_iter().enumerate() {
let path = Path::new(&item);
// 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(&item, 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(&item, 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,
},
]
}
}