mirror of
https://github.com/nushell/nushell.git
synced 2025-02-03 12:09:38 +01:00
Merge branch 'main' of github.com:PegasusPlusUS/nushell
This commit is contained in:
commit
dd3adbff89
62
Cargo.lock
generated
62
Cargo.lock
generated
@ -3229,6 +3229,7 @@ dependencies = [
|
|||||||
"uu_mkdir",
|
"uu_mkdir",
|
||||||
"uu_mktemp",
|
"uu_mktemp",
|
||||||
"uu_mv",
|
"uu_mv",
|
||||||
|
"uu_touch",
|
||||||
"uu_uname",
|
"uu_uname",
|
||||||
"uu_whoami",
|
"uu_whoami",
|
||||||
"uucore",
|
"uucore",
|
||||||
@ -4084,6 +4085,17 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse_datetime"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"nom",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -6700,9 +6712,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_cp"
|
name = "uu_cp"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fb99d355ccb02e8c514e4a1d93e4aa4eedea9837de24635dfd24c165971444e"
|
checksum = "e0eff79f5eacf6bb88c9afc19f3cec2ab14ad31317be1369100658b46d41e410"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"filetime",
|
"filetime",
|
||||||
@ -6716,9 +6728,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_mkdir"
|
name = "uu_mkdir"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "219588fbc146f18188781208ac4034616c51cf151677b4e1f9caf63ca8a7f2cf"
|
checksum = "feba7cf875eecbb746b1c5a5a8a031ab3a00e5f44f5441643a06b78577780d3a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"uucore",
|
"uucore",
|
||||||
@ -6726,9 +6738,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_mktemp"
|
name = "uu_mktemp"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1e79ad2c5911908fce23a6069c52ca82e1997e2ed4bf6abf2d867c79c3dc73f"
|
checksum = "1a9cfd389f60e667c5ee6659beaad50bada7e710d76082c7d77ab91e04307c8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"rand",
|
"rand",
|
||||||
@ -6738,9 +6750,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_mv"
|
name = "uu_mv"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd57c8d02f8a99ed56ed9f6fddab403ee0e2bf9e8f3a5ca8f0f9e4d6e3e392a0"
|
checksum = "bf932231fccdf108f75443bab0ce17acfe49b5825d731b8a358251833be7da20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
@ -6749,10 +6761,24 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_uname"
|
name = "uu_touch"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad1ca90f9b292bccaad0de70e6feccac5182c6713a5e1ca72d97bf3555b608b4"
|
checksum = "55476bec11d5b70c578233a2e94f685058e0d65fc5d66c7ed465877c15124c7c"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"filetime",
|
||||||
|
"parse_datetime",
|
||||||
|
"uucore",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uu_uname"
|
||||||
|
version = "0.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "182b4071a2e6f7288cbbc1b1ff05c74e9dc7527b4735583d9e3cd92802b06910"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"platform-info",
|
"platform-info",
|
||||||
@ -6761,27 +6787,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_whoami"
|
name = "uu_whoami"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc7c52e42e0425710461700adc1063f468f2ba8a8ff83ee69ba661095ab7b77a"
|
checksum = "5d15200414428c65f95d0b1d1226fc84f74ae80376bfe59959d93ddf57f944f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"libc",
|
"libc",
|
||||||
"uucore",
|
"uucore",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uucore"
|
name = "uucore"
|
||||||
version = "0.0.27"
|
version = "0.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b54aad02cf7e96f5fafabb6b836efa73eef934783b17530095a29ffd4fdc154"
|
checksum = "04ea43050c46912575654c5181f4135529e8d4003fca80803af10cdef3ca6412"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"libc",
|
"libc",
|
||||||
"nix 0.28.0",
|
"nix 0.29.0",
|
||||||
"number_prefix",
|
"number_prefix",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"os_display",
|
"os_display",
|
||||||
@ -6789,7 +6815,7 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
"wild",
|
"wild",
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.59.0",
|
||||||
"xattr",
|
"xattr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
15
Cargo.toml
15
Cargo.toml
@ -165,13 +165,14 @@ unicode-segmentation = "1.12"
|
|||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
ureq = { version = "2.10", default-features = false }
|
ureq = { version = "2.10", default-features = false }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
uu_cp = "0.0.27"
|
uu_cp = "0.0.28"
|
||||||
uu_mkdir = "0.0.27"
|
uu_mkdir = "0.0.28"
|
||||||
uu_mktemp = "0.0.27"
|
uu_mktemp = "0.0.28"
|
||||||
uu_mv = "0.0.27"
|
uu_mv = "0.0.28"
|
||||||
uu_whoami = "0.0.27"
|
uu_touch = "0.0.28"
|
||||||
uu_uname = "0.0.27"
|
uu_whoami = "0.0.28"
|
||||||
uucore = "0.0.27"
|
uu_uname = "0.0.28"
|
||||||
|
uucore = "0.0.28"
|
||||||
uuid = "1.11.0"
|
uuid = "1.11.0"
|
||||||
v_htmlescape = "0.15.0"
|
v_htmlescape = "0.15.0"
|
||||||
wax = "0.6"
|
wax = "0.6"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::NushellPrompt;
|
use crate::NushellPrompt;
|
||||||
use log::trace;
|
use log::{trace, warn};
|
||||||
use nu_engine::ClosureEvalOnce;
|
use nu_engine::ClosureEvalOnce;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack},
|
engine::{EngineState, Stack},
|
||||||
@ -80,8 +80,13 @@ fn get_prompt_string(
|
|||||||
})
|
})
|
||||||
.and_then(|pipeline_data| {
|
.and_then(|pipeline_data| {
|
||||||
let output = pipeline_data.collect_string("", config).ok();
|
let output = pipeline_data.collect_string("", config).ok();
|
||||||
|
let ansi_output = output.map(|mut x| {
|
||||||
|
// Always reset the color at the start of the right prompt
|
||||||
|
// to ensure there is no ansi bleed over
|
||||||
|
if x.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
|
||||||
|
x.insert_str(0, "\x1b[0m")
|
||||||
|
};
|
||||||
|
|
||||||
output.map(|mut x| {
|
|
||||||
// Just remove the very last newline.
|
// Just remove the very last newline.
|
||||||
if x.ends_with('\n') {
|
if x.ends_with('\n') {
|
||||||
x.pop();
|
x.pop();
|
||||||
@ -91,7 +96,11 @@ fn get_prompt_string(
|
|||||||
x.pop();
|
x.pop();
|
||||||
}
|
}
|
||||||
x
|
x
|
||||||
})
|
});
|
||||||
|
// Let's keep this for debugging purposes with nu --log-level warn
|
||||||
|
warn!("{}:{}:{} {:?}", file!(), line!(), column!(), ansi_output);
|
||||||
|
|
||||||
|
ansi_output
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ uu_cp = { workspace = true }
|
|||||||
uu_mkdir = { workspace = true }
|
uu_mkdir = { workspace = true }
|
||||||
uu_mktemp = { workspace = true }
|
uu_mktemp = { workspace = true }
|
||||||
uu_mv = { workspace = true }
|
uu_mv = { workspace = true }
|
||||||
|
uu_touch = { workspace = true }
|
||||||
uu_uname = { workspace = true }
|
uu_uname = { workspace = true }
|
||||||
uu_whoami = { workspace = true }
|
uu_whoami = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
@ -230,6 +230,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
Rm,
|
Rm,
|
||||||
Save,
|
Save,
|
||||||
Touch,
|
Touch,
|
||||||
|
UTouch,
|
||||||
Glob,
|
Glob,
|
||||||
Watch,
|
Watch,
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@ mod ucp;
|
|||||||
mod umkdir;
|
mod umkdir;
|
||||||
mod umv;
|
mod umv;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod utouch;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
pub use self::open::Open;
|
pub use self::open::Open;
|
||||||
@ -27,4 +28,5 @@ pub use touch::Touch;
|
|||||||
pub use ucp::UCp;
|
pub use ucp::UCp;
|
||||||
pub use umkdir::UMkdir;
|
pub use umkdir::UMkdir;
|
||||||
pub use umv::UMv;
|
pub use umv::UMv;
|
||||||
|
pub use utouch::UTouch;
|
||||||
pub use watch::Watch;
|
pub use watch::Watch;
|
||||||
|
@ -188,6 +188,7 @@ impl Command for UMv {
|
|||||||
target_dir: None,
|
target_dir: None,
|
||||||
no_target_dir: false,
|
no_target_dir: false,
|
||||||
strip_slashes: false,
|
strip_slashes: false,
|
||||||
|
debug: false,
|
||||||
};
|
};
|
||||||
if let Err(error) = uu_mv::mv(&files, &options) {
|
if let Err(error) = uu_mv::mv(&files, &options) {
|
||||||
return Err(ShellError::GenericError {
|
return Err(ShellError::GenericError {
|
||||||
|
268
crates/nu-command/src/filesystem/utouch.rs
Normal file
268
crates/nu-command/src/filesystem/utouch.rs
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
use filetime::FileTime;
|
||||||
|
|
||||||
|
use nu_engine::CallExt;
|
||||||
|
use nu_path::expand_path_with;
|
||||||
|
use nu_protocol::engine::{Call, Command, EngineState, Stack};
|
||||||
|
use nu_protocol::{
|
||||||
|
Category, Example, NuGlob, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type,
|
||||||
|
};
|
||||||
|
use uu_touch::error::TouchError;
|
||||||
|
use uu_touch::{ChangeTimes, InputFile, Options, Source};
|
||||||
|
|
||||||
|
use super::util::get_rest_for_glob_pattern;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UTouch;
|
||||||
|
|
||||||
|
impl Command for UTouch {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"utouch"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["create", "file"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("utouch")
|
||||||
|
.input_output_types(vec![ (Type::Nothing, Type::Nothing) ])
|
||||||
|
.rest(
|
||||||
|
"files",
|
||||||
|
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Filepath]),
|
||||||
|
"The file(s) to create. '-' is used to represent stdout."
|
||||||
|
)
|
||||||
|
.named(
|
||||||
|
"reference",
|
||||||
|
SyntaxShape::Filepath,
|
||||||
|
"Use the access and modification times of the reference file/directory instead of the current time",
|
||||||
|
Some('r'),
|
||||||
|
)
|
||||||
|
.named(
|
||||||
|
"timestamp",
|
||||||
|
SyntaxShape::DateTime,
|
||||||
|
"Use the given timestamp instead of the current time",
|
||||||
|
Some('t')
|
||||||
|
)
|
||||||
|
.named(
|
||||||
|
"date",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"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",
|
||||||
|
Some('d')
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"modified",
|
||||||
|
"Change only the modification time (if used with -a, access time is changed too)",
|
||||||
|
Some('m'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"access",
|
||||||
|
"Change only the access time (if used with -m, modification time is changed too)",
|
||||||
|
Some('a'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"no-create",
|
||||||
|
"Don't create the file if it doesn't exist",
|
||||||
|
Some('c'),
|
||||||
|
)
|
||||||
|
.switch(
|
||||||
|
"no-deref",
|
||||||
|
"Affect each symbolic link instead of any referenced file (only for systems that can change the timestamps of a symlink). Ignored if touching stdout",
|
||||||
|
Some('s'),
|
||||||
|
)
|
||||||
|
.category(Category::FileSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Creates one or more files."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let change_mtime: bool = call.has_flag(engine_state, stack, "modified")?;
|
||||||
|
let change_atime: bool = call.has_flag(engine_state, stack, "access")?;
|
||||||
|
let no_create: bool = call.has_flag(engine_state, stack, "no-create")?;
|
||||||
|
let no_deref: bool = call.has_flag(engine_state, stack, "no-dereference")?;
|
||||||
|
let file_globs: Vec<Spanned<NuGlob>> =
|
||||||
|
get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
||||||
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
|
|
||||||
|
if file_globs.is_empty() {
|
||||||
|
return Err(ShellError::MissingParameter {
|
||||||
|
param_name: "requires file paths".to_string(),
|
||||||
|
span: call.head,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let (reference_file, reference_span) = if let Some(reference) =
|
||||||
|
call.get_flag::<Spanned<PathBuf>>(engine_state, stack, "reference")?
|
||||||
|
{
|
||||||
|
(Some(reference.item), Some(reference.span))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
let (date_str, date_span) =
|
||||||
|
if let Some(date) = call.get_flag::<Spanned<String>>(engine_state, stack, "date")? {
|
||||||
|
(Some(date.item), Some(date.span))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
let timestamp: Option<Spanned<DateTime<FixedOffset>>> =
|
||||||
|
call.get_flag(engine_state, stack, "timestamp")?;
|
||||||
|
|
||||||
|
let source = if let Some(timestamp) = timestamp {
|
||||||
|
if let Some(reference_span) = reference_span {
|
||||||
|
return Err(ShellError::IncompatibleParameters {
|
||||||
|
left_message: "timestamp given".to_string(),
|
||||||
|
left_span: timestamp.span,
|
||||||
|
right_message: "reference given".to_string(),
|
||||||
|
right_span: reference_span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(date_span) = date_span {
|
||||||
|
return Err(ShellError::IncompatibleParameters {
|
||||||
|
left_message: "timestamp given".to_string(),
|
||||||
|
left_span: timestamp.span,
|
||||||
|
right_message: "date given".to_string(),
|
||||||
|
right_span: date_span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Source::Timestamp(FileTime::from_unix_time(
|
||||||
|
timestamp.item.timestamp(),
|
||||||
|
timestamp.item.timestamp_subsec_nanos(),
|
||||||
|
))
|
||||||
|
} else if let Some(reference_file) = reference_file {
|
||||||
|
let reference_file = expand_path_with(reference_file, &cwd, true);
|
||||||
|
Source::Reference(reference_file)
|
||||||
|
} else {
|
||||||
|
Source::Now
|
||||||
|
};
|
||||||
|
|
||||||
|
let change_times = if change_atime && !change_mtime {
|
||||||
|
ChangeTimes::AtimeOnly
|
||||||
|
} else if change_mtime && !change_atime {
|
||||||
|
ChangeTimes::MtimeOnly
|
||||||
|
} else {
|
||||||
|
ChangeTimes::Both
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut input_files = Vec::new();
|
||||||
|
for file_glob in &file_globs {
|
||||||
|
if file_glob.item.as_ref() == "-" {
|
||||||
|
input_files.push(InputFile::Stdout);
|
||||||
|
} else {
|
||||||
|
let path =
|
||||||
|
expand_path_with(file_glob.item.as_ref(), &cwd, file_glob.item.is_expand());
|
||||||
|
input_files.push(InputFile::Path(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = uu_touch::touch(
|
||||||
|
&input_files,
|
||||||
|
&Options {
|
||||||
|
no_create,
|
||||||
|
no_deref,
|
||||||
|
source,
|
||||||
|
date: date_str,
|
||||||
|
change_times,
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
let nu_err = match err {
|
||||||
|
TouchError::TouchFileError { path, index, error } => ShellError::GenericError {
|
||||||
|
error: format!("Could not touch {}", path.display()),
|
||||||
|
msg: error.to_string(),
|
||||||
|
span: Some(file_globs[index].span),
|
||||||
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
|
},
|
||||||
|
TouchError::InvalidDateFormat(date) => ShellError::IncorrectValue {
|
||||||
|
msg: format!("Invalid date: {}", date),
|
||||||
|
val_span: date_span.expect("utouch should've been given a date"),
|
||||||
|
call_span: call.head,
|
||||||
|
},
|
||||||
|
TouchError::ReferenceFileInaccessible(reference_path, io_err) => {
|
||||||
|
let span =
|
||||||
|
reference_span.expect("utouch should've been given a reference file");
|
||||||
|
if io_err.kind() == ErrorKind::NotFound {
|
||||||
|
ShellError::FileNotFound {
|
||||||
|
span,
|
||||||
|
file: reference_path.display().to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ShellError::GenericError {
|
||||||
|
error: io_err.to_string(),
|
||||||
|
msg: format!("Failed to read metadata of {}", reference_path.display()),
|
||||||
|
span: Some(span),
|
||||||
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ShellError::GenericError {
|
||||||
|
error: err.to_string(),
|
||||||
|
msg: err.to_string(),
|
||||||
|
span: Some(call.head),
|
||||||
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return Err(nu_err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PipelineData::empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Creates \"fixture.json\"",
|
||||||
|
example: "utouch fixture.json",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Creates files a, b and c",
|
||||||
|
example: "utouch a b c",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: r#"Changes the last modified time of "fixture.json" to today's date"#,
|
||||||
|
example: "utouch -m fixture.json",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Changes the last accessed and modified times of files a, b and c to the current time but yesterday",
|
||||||
|
example: r#"utouch -d "yesterday" a b c"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: r#"Changes the last modified time of files d and e to "fixture.json"'s last modified time"#,
|
||||||
|
example: r#"utouch -m -r fixture.json d e"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: r#"Changes the last accessed time of "fixture.json" to a datetime"#,
|
||||||
|
example: r#"utouch -a -t 2019-08-24T12:30:30 fixture.json"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: r#"Change the last accessed and modified times of stdout"#,
|
||||||
|
example: r#"utouch -"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: r#"Changes the last accessed and modified times of file a to 1 month before "fixture.json"'s last modified time"#,
|
||||||
|
example: r#"utouch -r fixture.json -d "-1 month" a"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use nu_engine::{command_prelude::*, ClosureEval};
|
use nu_engine::{command_prelude::*, ClosureEval};
|
||||||
use nu_protocol::{engine::Closure, IntoValue};
|
use nu_protocol::{engine::Closure, FromValue, IntoValue};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GroupBy;
|
pub struct GroupBy;
|
||||||
@ -12,10 +12,6 @@ impl Command for GroupBy {
|
|||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("group-by")
|
Signature::build("group-by")
|
||||||
// TODO: It accepts Table also, but currently there is no Table
|
|
||||||
// example. Perhaps Table should be a subtype of List, in which case
|
|
||||||
// the current signature would suffice even when a Table example
|
|
||||||
// exists.
|
|
||||||
.input_output_types(vec![(Type::List(Box::new(Type::Any)), Type::Any)])
|
.input_output_types(vec![(Type::List(Box::new(Type::Any)), Type::Any)])
|
||||||
.switch(
|
.switch(
|
||||||
"to-table",
|
"to-table",
|
||||||
@ -229,7 +225,7 @@ pub fn group_by(
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let head = call.head;
|
let head = call.head;
|
||||||
let groupers: Vec<Value> = call.rest(engine_state, stack, 0)?;
|
let groupers: Vec<Spanned<Grouper>> = call.rest(engine_state, stack, 0)?;
|
||||||
let to_table = call.has_flag(engine_state, stack, "to-table")?;
|
let to_table = call.has_flag(engine_state, stack, "to-table")?;
|
||||||
let config = engine_state.get_config();
|
let config = engine_state.get_config();
|
||||||
|
|
||||||
@ -238,20 +234,20 @@ pub fn group_by(
|
|||||||
return Ok(Value::record(Record::new(), head).into_pipeline_data());
|
return Ok(Value::record(Record::new(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut groupers = groupers.into_iter();
|
let grouped = match &groupers[..] {
|
||||||
|
[first, rest @ ..] => {
|
||||||
let grouped = if let Some(grouper) = groupers.next() {
|
let mut grouped = Grouped::new(first.as_ref(), values, config, engine_state, stack)?;
|
||||||
let mut groups = Grouped::new(&grouper, values, config, engine_state, stack)?;
|
for grouper in rest {
|
||||||
for grouper in groupers {
|
grouped.subgroup(grouper.as_ref(), config, engine_state, stack)?;
|
||||||
groups.subgroup(&grouper, config, engine_state, stack)?;
|
}
|
||||||
|
grouped
|
||||||
}
|
}
|
||||||
groups
|
[] => Grouped::empty(values, config),
|
||||||
} else {
|
|
||||||
Grouped::empty(values, config)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = if to_table {
|
let value = if to_table {
|
||||||
grouped.into_table(head)
|
let column_names = groupers_to_column_names(&groupers)?;
|
||||||
|
grouped.into_table(&column_names, head)
|
||||||
} else {
|
} else {
|
||||||
grouped.into_record(head)
|
grouped.into_record(head)
|
||||||
};
|
};
|
||||||
@ -259,8 +255,67 @@ pub fn group_by(
|
|||||||
Ok(value.into_pipeline_data())
|
Ok(value.into_pipeline_data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn groupers_to_column_names(groupers: &[Spanned<Grouper>]) -> Result<Vec<String>, ShellError> {
|
||||||
|
if groupers.is_empty() {
|
||||||
|
return Ok(vec!["group".into(), "items".into()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut closure_idx: usize = 0;
|
||||||
|
let grouper_names = groupers.iter().map(|grouper| {
|
||||||
|
grouper.as_ref().map(|item| match item {
|
||||||
|
Grouper::CellPath { val } => val.to_column_name(),
|
||||||
|
Grouper::Closure { .. } => {
|
||||||
|
closure_idx += 1;
|
||||||
|
format!("closure_{}", closure_idx - 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut name_set: Vec<Spanned<String>> = Vec::with_capacity(grouper_names.len());
|
||||||
|
|
||||||
|
for name in grouper_names {
|
||||||
|
if name.item == "items" {
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: "grouper arguments can't be named `items`".into(),
|
||||||
|
msg: "here".into(),
|
||||||
|
span: Some(name.span),
|
||||||
|
help: Some("instead of a cell-path, try using a closure: { get items }".into()),
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(conflicting_name) = name_set
|
||||||
|
.iter()
|
||||||
|
.find(|elem| elem.as_ref().item == name.item.as_str())
|
||||||
|
{
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: "grouper arguments result in colliding column names".into(),
|
||||||
|
msg: "duplicate column names".into(),
|
||||||
|
span: Some(conflicting_name.span.append(name.span)),
|
||||||
|
help: Some(
|
||||||
|
"instead of a cell-path, try using a closure or renaming columns".into(),
|
||||||
|
),
|
||||||
|
inner: vec![ShellError::ColumnDefinedTwice {
|
||||||
|
col_name: conflicting_name.item.clone(),
|
||||||
|
first_use: conflicting_name.span,
|
||||||
|
second_use: name.span,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
name_set.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let column_names: Vec<String> = name_set
|
||||||
|
.into_iter()
|
||||||
|
.map(|elem| elem.item)
|
||||||
|
.chain(["items".into()])
|
||||||
|
.collect();
|
||||||
|
Ok(column_names)
|
||||||
|
}
|
||||||
|
|
||||||
fn group_cell_path(
|
fn group_cell_path(
|
||||||
column_name: CellPath,
|
column_name: &CellPath,
|
||||||
values: Vec<Value>,
|
values: Vec<Value>,
|
||||||
config: &nu_protocol::Config,
|
config: &nu_protocol::Config,
|
||||||
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
|
||||||
@ -305,8 +360,25 @@ fn group_closure(
|
|||||||
Ok(groups)
|
Ok(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Grouper {
|
||||||
|
CellPath { val: CellPath },
|
||||||
|
Closure { val: Box<Closure> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromValue for Grouper {
|
||||||
|
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||||
|
match v {
|
||||||
|
Value::CellPath { val, .. } => Ok(Grouper::CellPath { val }),
|
||||||
|
Value::Closure { val, .. } => Ok(Grouper::Closure { val }),
|
||||||
|
_ => Err(ShellError::TypeMismatch {
|
||||||
|
err_message: "unsupported grouper type".to_string(),
|
||||||
|
span: v.span(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Grouped {
|
struct Grouped {
|
||||||
grouper: Option<String>,
|
|
||||||
groups: Tree,
|
groups: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,41 +397,35 @@ impl Grouped {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
grouper: Some("group".into()),
|
|
||||||
groups: Tree::Leaf(groups),
|
groups: Tree::Leaf(groups),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
grouper: &Value,
|
grouper: Spanned<&Grouper>,
|
||||||
values: Vec<Value>,
|
values: Vec<Value>,
|
||||||
config: &nu_protocol::Config,
|
config: &nu_protocol::Config,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
) -> Result<Self, ShellError> {
|
) -> Result<Self, ShellError> {
|
||||||
let span = grouper.span();
|
let groups = match grouper.item {
|
||||||
let groups = match grouper {
|
Grouper::CellPath { val } => group_cell_path(val, values, config)?,
|
||||||
Value::CellPath { val, .. } => group_cell_path(val.clone(), values, config)?,
|
Grouper::Closure { val } => group_closure(
|
||||||
Value::Closure { val, .. } => {
|
values,
|
||||||
group_closure(values, span, Closure::clone(val), engine_state, stack)?
|
grouper.span,
|
||||||
}
|
Closure::clone(val),
|
||||||
_ => {
|
engine_state,
|
||||||
return Err(ShellError::TypeMismatch {
|
stack,
|
||||||
err_message: "unsupported grouper type".to_string(),
|
)?,
|
||||||
span,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let grouper = grouper.as_cell_path().ok().map(CellPath::to_column_name);
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
grouper,
|
|
||||||
groups: Tree::Leaf(groups),
|
groups: Tree::Leaf(groups),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subgroup(
|
fn subgroup(
|
||||||
&mut self,
|
&mut self,
|
||||||
grouper: &Value,
|
grouper: Spanned<&Grouper>,
|
||||||
config: &nu_protocol::Config,
|
config: &nu_protocol::Config,
|
||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
@ -384,34 +450,33 @@ impl Grouped {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_table(self, head: Span) -> Value {
|
fn into_table(self, column_names: &[String], head: Span) -> Value {
|
||||||
self._into_table(head, 0)
|
self._into_table(head)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| row.into_iter().rev().collect::<Record>().into_value(head))
|
.map(|row| {
|
||||||
|
row.into_iter()
|
||||||
|
.rev()
|
||||||
|
.zip(column_names)
|
||||||
|
.map(|(val, key)| (key.clone(), val))
|
||||||
|
.collect::<Record>()
|
||||||
|
.into_value(head)
|
||||||
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_value(head)
|
.into_value(head)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _into_table(self, head: Span, index: usize) -> Vec<Record> {
|
fn _into_table(self, head: Span) -> Vec<Vec<Value>> {
|
||||||
let grouper = self.grouper.unwrap_or_else(|| format!("group{index}"));
|
|
||||||
match self.groups {
|
match self.groups {
|
||||||
Tree::Leaf(leaf) => leaf
|
Tree::Leaf(leaf) => leaf
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(group, values)| {
|
.map(|(group, values)| vec![(values.into_value(head)), (group.into_value(head))])
|
||||||
[
|
.collect::<Vec<Vec<Value>>>(),
|
||||||
("items".to_string(), values.into_value(head)),
|
|
||||||
(grouper.clone(), group.into_value(head)),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect::<Vec<Record>>(),
|
|
||||||
Tree::Branch(branch) => branch
|
Tree::Branch(branch) => branch
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|(group, items)| {
|
.flat_map(|(group, items)| {
|
||||||
let mut inner = items._into_table(head, index + 1);
|
let mut inner = items._into_table(head);
|
||||||
for row in &mut inner {
|
for row in &mut inner {
|
||||||
row.insert(grouper.clone(), group.clone().into_value(head));
|
row.push(group.clone().into_value(head));
|
||||||
}
|
}
|
||||||
inner
|
inner
|
||||||
})
|
})
|
||||||
|
@ -76,7 +76,7 @@ impl Command for External {
|
|||||||
// believe the user wants to use the windows association to run the script. The only
|
// believe the user wants to use the windows association to run the script. The only
|
||||||
// easy way to do this is to run cmd.exe with the script as an argument.
|
// easy way to do this is to run cmd.exe with the script as an argument.
|
||||||
let potential_nuscript_in_windows = if cfg!(windows) {
|
let potential_nuscript_in_windows = if cfg!(windows) {
|
||||||
// let's make sure it's a .nu scrtipt
|
// let's make sure it's a .nu script
|
||||||
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
|
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
|
||||||
let ext = executable
|
let ext = executable
|
||||||
.extension()
|
.extension()
|
||||||
|
@ -2,6 +2,13 @@ use nu_test_support::nu;
|
|||||||
use nu_test_support::playground::Playground;
|
use nu_test_support::playground::Playground;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_with_trailing_comma() {
|
||||||
|
let actual = nu!("def test-command [ foo: int, ] { $foo }; test-command 1");
|
||||||
|
|
||||||
|
assert!(actual.out == "1");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_with_comment() {
|
fn def_with_comment() {
|
||||||
Playground::setup("def_with_comment", |dirs, _| {
|
Playground::setup("def_with_comment", |dirs, _| {
|
||||||
@ -72,6 +79,13 @@ fn def_errors_with_comma_before_equals() {
|
|||||||
assert!(actual.err.contains("expected parameter"));
|
assert!(actual.err.contains("expected parameter"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_errors_with_colon_before_equals() {
|
||||||
|
let actual = nu!("def test-command [ foo: = 1 ] {}");
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected type"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_errors_with_comma_before_colon() {
|
fn def_errors_with_comma_before_colon() {
|
||||||
let actual = nu!("def test-command [ foo, : int ] {}");
|
let actual = nu!("def test-command [ foo, : int ] {}");
|
||||||
@ -85,7 +99,6 @@ fn def_errors_with_multiple_colons() {
|
|||||||
assert!(actual.err.contains("expected type"));
|
assert!(actual.err.contains("expected type"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore = "This error condition is not implemented yet"]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_errors_with_multiple_types() {
|
fn def_errors_with_multiple_types() {
|
||||||
let actual = nu!("def test-command [ foo:int:string ] {}");
|
let actual = nu!("def test-command [ foo:int:string ] {}");
|
||||||
@ -93,6 +106,20 @@ fn def_errors_with_multiple_types() {
|
|||||||
assert!(actual.err.contains("expected parameter"));
|
assert!(actual.err.contains("expected parameter"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_errors_with_trailing_colon() {
|
||||||
|
let actual = nu!("def test-command [ foo: int: ] {}");
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected parameter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn def_errors_with_trailing_default_value() {
|
||||||
|
let actual = nu!("def test-command [ foo: int = ] {}");
|
||||||
|
|
||||||
|
assert!(actual.err.contains("expected default value"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn def_errors_with_multiple_commas() {
|
fn def_errors_with_multiple_commas() {
|
||||||
let actual = nu!("def test-command [ foo,,bar ] {}");
|
let actual = nu!("def test-command [ foo,,bar ] {}");
|
||||||
|
@ -127,6 +127,7 @@ mod update;
|
|||||||
mod upsert;
|
mod upsert;
|
||||||
mod url;
|
mod url;
|
||||||
mod use_;
|
mod use_;
|
||||||
|
mod utouch;
|
||||||
mod where_;
|
mod where_;
|
||||||
mod which;
|
mod which;
|
||||||
mod while_;
|
mod while_;
|
||||||
|
@ -513,13 +513,18 @@ fn test_mv_no_clobber() {
|
|||||||
sandbox.with_files(&[EmptyFile(file_a)]);
|
sandbox.with_files(&[EmptyFile(file_a)]);
|
||||||
sandbox.with_files(&[EmptyFile(file_b)]);
|
sandbox.with_files(&[EmptyFile(file_b)]);
|
||||||
|
|
||||||
let actual = nu!(
|
let _ = nu!(
|
||||||
cwd: dirs.test(),
|
cwd: dirs.test(),
|
||||||
"mv -n {} {}",
|
"mv -n {} {}",
|
||||||
file_a,
|
file_a,
|
||||||
file_b,
|
file_b,
|
||||||
);
|
);
|
||||||
assert!(actual.err.contains("not replacing"));
|
|
||||||
|
let file_count = nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"ls test_mv* | length | to nuon"
|
||||||
|
);
|
||||||
|
assert_eq!(file_count.out, "2");
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -841,14 +841,13 @@ fn test_cp_arg_no_clobber() {
|
|||||||
let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE);
|
let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE);
|
||||||
let target_hash = get_file_hash(target.display());
|
let target_hash = get_file_hash(target.display());
|
||||||
|
|
||||||
let actual = nu!(
|
let _ = nu!(
|
||||||
cwd: dirs.root(),
|
cwd: dirs.root(),
|
||||||
"cp {} {} --no-clobber",
|
"cp {} {} --no-clobber",
|
||||||
src.display(),
|
src.display(),
|
||||||
target.display()
|
target.display()
|
||||||
);
|
);
|
||||||
let after_cp_hash = get_file_hash(target.display());
|
let after_cp_hash = get_file_hash(target.display());
|
||||||
assert!(actual.err.contains("not replacing"));
|
|
||||||
// Check content was not clobbered
|
// Check content was not clobbered
|
||||||
assert_eq!(after_cp_hash, target_hash);
|
assert_eq!(after_cp_hash, target_hash);
|
||||||
});
|
});
|
||||||
|
740
crates/nu-command/tests/commands/utouch.rs
Normal file
740
crates/nu-command/tests/commands/utouch.rs
Normal file
@ -0,0 +1,740 @@
|
|||||||
|
use chrono::{DateTime, Days, Local, TimeDelta, Utc};
|
||||||
|
use filetime::FileTime;
|
||||||
|
use nu_test_support::fs::{files_exist_at, Stub};
|
||||||
|
use nu_test_support::nu;
|
||||||
|
use nu_test_support::playground::{Dirs, Playground};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// Use 1 instead of 0 because 0 has a special meaning in Windows
|
||||||
|
const TIME_ONE: FileTime = FileTime::from_unix_time(1, 0);
|
||||||
|
|
||||||
|
fn file_times(file: impl AsRef<Path>) -> (FileTime, FileTime) {
|
||||||
|
(
|
||||||
|
file.as_ref().metadata().unwrap().accessed().unwrap().into(),
|
||||||
|
file.as_ref().metadata().unwrap().modified().unwrap().into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symlink_times(path: &nu_path::AbsolutePath) -> (filetime::FileTime, filetime::FileTime) {
|
||||||
|
let metadata = path.symlink_metadata().unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
filetime::FileTime::from_system_time(metadata.accessed().unwrap()),
|
||||||
|
filetime::FileTime::from_system_time(metadata.modified().unwrap()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/nushell/nushell/pull/14214
|
||||||
|
fn setup_symlink_fs(dirs: &Dirs, sandbox: &mut Playground<'_>) {
|
||||||
|
sandbox.mkdir("d");
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("f"), Stub::EmptyFile("d/f")]);
|
||||||
|
sandbox.symlink("f", "fs");
|
||||||
|
sandbox.symlink("d", "ds");
|
||||||
|
sandbox.symlink("d/f", "fds");
|
||||||
|
|
||||||
|
// sandbox.symlink does not handle symlinks to missing files well. It panics
|
||||||
|
// But they are useful, and they should be tested.
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
std::os::unix::fs::symlink(dirs.test().join("m"), dirs.test().join("fms")).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
std::os::windows::fs::symlink_file(dirs.test().join("m"), dirs.test().join("fms")).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the file times to a known "old" value for comparison
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("f"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("d"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("d/f"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("ds"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("fs"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("fds"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
filetime::set_symlink_file_times(dirs.test().join("fms"), TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_a_file_when_it_doesnt_exist() {
|
||||||
|
Playground::setup("create_test_1", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch i_will_be_created.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("i_will_be_created.txt");
|
||||||
|
assert!(path.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_two_files() {
|
||||||
|
Playground::setup("create_test_2", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch a b"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("a");
|
||||||
|
assert!(path.exists());
|
||||||
|
|
||||||
|
let path2 = dirs.test().join("b");
|
||||||
|
assert!(path2.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_modified_time_of_file_to_today() {
|
||||||
|
Playground::setup("change_time_test_9", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
|
||||||
|
let path = dirs.test().join("file.txt");
|
||||||
|
|
||||||
|
// Set file.txt's times to the past before the test to make sure `utouch` actually changes the mtime to today
|
||||||
|
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -m file.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = path.metadata().unwrap();
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, mtime_day);
|
||||||
|
|
||||||
|
// Check that atime remains unchanged
|
||||||
|
assert_eq!(
|
||||||
|
TIME_ONE,
|
||||||
|
FileTime::from_system_time(metadata.accessed().unwrap())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_access_time_of_file_to_today() {
|
||||||
|
Playground::setup("change_time_test_18", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
|
||||||
|
let path = dirs.test().join("file.txt");
|
||||||
|
|
||||||
|
// Set file.txt's times to the past before the test to make sure `utouch` actually changes the atime to today
|
||||||
|
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -a file.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = path.metadata().unwrap();
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, atime_day);
|
||||||
|
|
||||||
|
// Check that mtime remains unchanged
|
||||||
|
assert_eq!(
|
||||||
|
TIME_ONE,
|
||||||
|
FileTime::from_system_time(metadata.modified().unwrap())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_modified_and_access_time_of_file_to_today() {
|
||||||
|
Playground::setup("change_time_test_27", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
|
||||||
|
let path = dirs.test().join("file.txt");
|
||||||
|
|
||||||
|
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -a -m file.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = path.metadata().unwrap();
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
|
||||||
|
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, mtime_day);
|
||||||
|
assert_eq!(today, atime_day);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_create_file_if_it_not_exists() {
|
||||||
|
Playground::setup("change_time_test_28", |dirs, _sandbox| {
|
||||||
|
let outcome = nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -c file.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("file.txt");
|
||||||
|
|
||||||
|
assert!(!path.exists());
|
||||||
|
|
||||||
|
// If --no-create is improperly handled `utouch` may error when trying to change the times of a nonexistent file
|
||||||
|
assert!(outcome.status.success())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_file_times_if_exists_with_no_create() {
|
||||||
|
Playground::setup(
|
||||||
|
"change_file_times_if_exists_with_no_create",
|
||||||
|
|dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("file.txt")]);
|
||||||
|
let path = dirs.test().join("file.txt");
|
||||||
|
|
||||||
|
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -c file.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = path.metadata().unwrap();
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
|
||||||
|
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, mtime_day);
|
||||||
|
assert_eq!(today, atime_day);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_file_three_dots() {
|
||||||
|
Playground::setup("create_test_1", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch file..."
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("file...");
|
||||||
|
assert!(path.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_file_four_dots() {
|
||||||
|
Playground::setup("create_test_1", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch file...."
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("file....");
|
||||||
|
assert!(path.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_file_four_dots_quotation_marks() {
|
||||||
|
Playground::setup("create_test_1", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch 'file....'"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("file....");
|
||||||
|
assert!(path.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_file_times_to_reference_file() {
|
||||||
|
Playground::setup("change_dir_times_to_reference_dir", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[
|
||||||
|
Stub::EmptyFile("reference_file"),
|
||||||
|
Stub::EmptyFile("target_file"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let reference = dirs.test().join("reference_file");
|
||||||
|
let target = dirs.test().join("target_file");
|
||||||
|
|
||||||
|
// Change the times for reference
|
||||||
|
filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
// target should have today's date since it was just created, but reference should be different
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -r reference_file target_file"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_file_mtime_to_reference() {
|
||||||
|
Playground::setup("change_file_mtime_to_reference", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[
|
||||||
|
Stub::EmptyFile("reference_file"),
|
||||||
|
Stub::EmptyFile("target_file"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let reference = dirs.test().join("reference_file");
|
||||||
|
let target = dirs.test().join("target_file");
|
||||||
|
|
||||||
|
// Change the times for reference
|
||||||
|
filetime::set_file_times(&reference, TIME_ONE, FileTime::from_unix_time(1337, 0)).unwrap();
|
||||||
|
|
||||||
|
// target should have today's date since it was just created, but reference should be different
|
||||||
|
assert_ne!(file_times(&reference), file_times(&target));
|
||||||
|
|
||||||
|
// Save target's current atime to make sure it is preserved
|
||||||
|
let target_original_atime = target.metadata().unwrap().accessed().unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -mr reference_file target_file"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target_original_atime,
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO when https://github.com/uutils/coreutils/issues/6629 is fixed,
|
||||||
|
// unignore this test
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn change_file_times_to_reference_file_with_date() {
|
||||||
|
Playground::setup(
|
||||||
|
"change_file_times_to_reference_file_with_date",
|
||||||
|
|dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[
|
||||||
|
Stub::EmptyFile("reference_file"),
|
||||||
|
Stub::EmptyFile("target_file"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let reference = dirs.test().join("reference_file");
|
||||||
|
let target = dirs.test().join("target_file");
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let ref_atime = now;
|
||||||
|
let ref_mtime = now.checked_sub_days(Days::new(5)).unwrap();
|
||||||
|
|
||||||
|
// Change the times for reference
|
||||||
|
filetime::set_file_times(
|
||||||
|
reference,
|
||||||
|
FileTime::from_unix_time(ref_atime.timestamp(), ref_atime.timestamp_subsec_nanos()),
|
||||||
|
FileTime::from_unix_time(ref_mtime.timestamp(), ref_mtime.timestamp_subsec_nanos()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
r#"utouch -r reference_file -d "yesterday" target_file"#
|
||||||
|
);
|
||||||
|
|
||||||
|
let (got_atime, got_mtime) = file_times(target);
|
||||||
|
let got = (
|
||||||
|
DateTime::from_timestamp(got_atime.seconds(), got_atime.nanoseconds()).unwrap(),
|
||||||
|
DateTime::from_timestamp(got_mtime.seconds(), got_mtime.nanoseconds()).unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
now.checked_sub_days(Days::new(1)).unwrap(),
|
||||||
|
now.checked_sub_days(Days::new(6)).unwrap()
|
||||||
|
),
|
||||||
|
got
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_file_times_to_timestamp() {
|
||||||
|
Playground::setup("change_file_times_to_timestamp", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("target_file")]);
|
||||||
|
|
||||||
|
let target = dirs.test().join("target_file");
|
||||||
|
let timestamp = DateTime::from_timestamp(TIME_ONE.unix_seconds(), TIME_ONE.nanoseconds())
|
||||||
|
.unwrap()
|
||||||
|
.to_rfc3339();
|
||||||
|
|
||||||
|
nu!(cwd: dirs.test(), format!("utouch --timestamp {} target_file", timestamp));
|
||||||
|
|
||||||
|
assert_eq!((TIME_ONE, TIME_ONE), file_times(target));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_modified_time_of_dir_to_today() {
|
||||||
|
Playground::setup("change_dir_mtime", |dirs, sandbox| {
|
||||||
|
sandbox.mkdir("test_dir");
|
||||||
|
let path = dirs.test().join("test_dir");
|
||||||
|
|
||||||
|
filetime::set_file_mtime(&path, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -m test_dir"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let mtime_day =
|
||||||
|
DateTime::<Local>::from(path.metadata().unwrap().modified().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, mtime_day);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_access_time_of_dir_to_today() {
|
||||||
|
Playground::setup("change_dir_atime", |dirs, sandbox| {
|
||||||
|
sandbox.mkdir("test_dir");
|
||||||
|
let path = dirs.test().join("test_dir");
|
||||||
|
|
||||||
|
filetime::set_file_atime(&path, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -a test_dir"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let atime_day =
|
||||||
|
DateTime::<Local>::from(path.metadata().unwrap().accessed().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, atime_day);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_modified_and_access_time_of_dir_to_today() {
|
||||||
|
Playground::setup("change_dir_times", |dirs, sandbox| {
|
||||||
|
sandbox.mkdir("test_dir");
|
||||||
|
let path = dirs.test().join("test_dir");
|
||||||
|
|
||||||
|
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -a -m test_dir"
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = path.metadata().unwrap();
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
|
||||||
|
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, mtime_day);
|
||||||
|
assert_eq!(today, atime_day);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO when https://github.com/uutils/coreutils/issues/6629 is fixed,
|
||||||
|
// unignore this test
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn change_file_times_to_date() {
|
||||||
|
Playground::setup("change_file_times_to_date", |dirs, sandbox| {
|
||||||
|
sandbox.with_files(&[Stub::EmptyFile("target_file")]);
|
||||||
|
|
||||||
|
let expected = Utc::now().checked_sub_signed(TimeDelta::hours(2)).unwrap();
|
||||||
|
nu!(cwd: dirs.test(), "utouch -d '-2 hours' target_file");
|
||||||
|
|
||||||
|
let (got_atime, got_mtime) = file_times(dirs.test().join("target_file"));
|
||||||
|
let got_atime =
|
||||||
|
DateTime::from_timestamp(got_atime.seconds(), got_atime.nanoseconds()).unwrap();
|
||||||
|
let got_mtime =
|
||||||
|
DateTime::from_timestamp(got_mtime.seconds(), got_mtime.nanoseconds()).unwrap();
|
||||||
|
let threshold = TimeDelta::minutes(1);
|
||||||
|
assert!(
|
||||||
|
got_atime.signed_duration_since(expected).lt(&threshold)
|
||||||
|
&& got_mtime.signed_duration_since(expected).lt(&threshold),
|
||||||
|
"Expected: {}. Got: atime={}, mtime={}",
|
||||||
|
expected,
|
||||||
|
got_atime,
|
||||||
|
got_mtime
|
||||||
|
);
|
||||||
|
assert!(got_mtime.signed_duration_since(expected).lt(&threshold));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_dir_three_dots_times() {
|
||||||
|
Playground::setup("change_dir_three_dots_times", |dirs, sandbox| {
|
||||||
|
sandbox.mkdir("test_dir...");
|
||||||
|
let path = dirs.test().join("test_dir...");
|
||||||
|
|
||||||
|
filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch test_dir..."
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = path.metadata().unwrap();
|
||||||
|
|
||||||
|
// Check only the date since the time may not match exactly
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let mtime_day = DateTime::<Local>::from(metadata.modified().unwrap()).date_naive();
|
||||||
|
let atime_day = DateTime::<Local>::from(metadata.accessed().unwrap()).date_naive();
|
||||||
|
|
||||||
|
assert_eq!(today, mtime_day);
|
||||||
|
assert_eq!(today, atime_day);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_dir_times_to_reference_dir() {
|
||||||
|
Playground::setup("change_dir_times_to_reference_dir", |dirs, sandbox| {
|
||||||
|
sandbox.mkdir("reference_dir");
|
||||||
|
sandbox.mkdir("target_dir");
|
||||||
|
|
||||||
|
let reference = dirs.test().join("reference_dir");
|
||||||
|
let target = dirs.test().join("target_dir");
|
||||||
|
|
||||||
|
// Change the times for reference
|
||||||
|
filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
// target should have today's date since it was just created, but reference should be different
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -r reference_dir target_dir"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_dir_atime_to_reference() {
|
||||||
|
Playground::setup("change_dir_atime_to_reference", |dirs, sandbox| {
|
||||||
|
sandbox.mkdir("reference_dir");
|
||||||
|
sandbox.mkdir("target_dir");
|
||||||
|
|
||||||
|
let reference = dirs.test().join("reference_dir");
|
||||||
|
let target = dirs.test().join("target_dir");
|
||||||
|
|
||||||
|
// Change the times for reference
|
||||||
|
filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap();
|
||||||
|
|
||||||
|
// target should have today's date since it was just created, but reference should be different
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save target's current mtime to make sure it is preserved
|
||||||
|
let target_original_mtime = target.metadata().unwrap().modified().unwrap();
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"utouch -ar reference_dir target_dir"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reference.metadata().unwrap().accessed().unwrap(),
|
||||||
|
target.metadata().unwrap().accessed().unwrap()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
reference.metadata().unwrap().modified().unwrap(),
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target_original_mtime,
|
||||||
|
target.metadata().unwrap().modified().unwrap()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_a_file_with_tilde() {
|
||||||
|
Playground::setup("utouch with tilde", |dirs, _| {
|
||||||
|
let actual = nu!(cwd: dirs.test(), "utouch '~tilde'");
|
||||||
|
assert!(actual.err.is_empty());
|
||||||
|
assert!(files_exist_at(&[Path::new("~tilde")], dirs.test()));
|
||||||
|
|
||||||
|
// pass variable
|
||||||
|
let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; utouch $f");
|
||||||
|
assert!(actual.err.is_empty());
|
||||||
|
assert!(files_exist_at(&[Path::new("~tilde2")], dirs.test()));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn respects_cwd() {
|
||||||
|
Playground::setup("utouch_respects_cwd", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"mkdir 'dir'; cd 'dir'; utouch 'i_will_be_created.txt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("dir/i_will_be_created.txt");
|
||||||
|
assert!(path.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reference_respects_cwd() {
|
||||||
|
Playground::setup("utouch_reference_respects_cwd", |dirs, _sandbox| {
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"mkdir 'dir'; cd 'dir'; utouch 'ref.txt'; utouch --reference 'ref.txt' 'foo.txt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = dirs.test().join("dir/foo.txt");
|
||||||
|
assert!(path.exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recognizes_stdout() {
|
||||||
|
Playground::setup("utouch_recognizes_stdout", |dirs, _sandbox| {
|
||||||
|
nu!(cwd: dirs.test(), "utouch -");
|
||||||
|
assert!(!dirs.test().join("-").exists());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn follow_symlinks() {
|
||||||
|
Playground::setup("touch_follows_symlinks", |dirs, sandbox| {
|
||||||
|
setup_symlink_fs(&dirs, sandbox);
|
||||||
|
|
||||||
|
let missing = dirs.test().join("m");
|
||||||
|
assert!(!missing.exists());
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"
|
||||||
|
touch fds
|
||||||
|
touch ds
|
||||||
|
touch fs
|
||||||
|
touch fms
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// We created the missing symlink target
|
||||||
|
assert!(missing.exists());
|
||||||
|
|
||||||
|
// The timestamps for files and directories were changed from TIME_ONE
|
||||||
|
let file_times = symlink_times(&dirs.test().join("f"));
|
||||||
|
let dir_times = symlink_times(&dirs.test().join("d"));
|
||||||
|
let dir_file_times = symlink_times(&dirs.test().join("d/f"));
|
||||||
|
|
||||||
|
assert_ne!(file_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_ne!(dir_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_ne!(dir_file_times, (TIME_ONE, TIME_ONE));
|
||||||
|
|
||||||
|
// For symlinks, they remain (mostly) the same
|
||||||
|
// We can't test accessed times, since to reach the target file, the symlink must be accessed!
|
||||||
|
let file_symlink_times = symlink_times(&dirs.test().join("fs"));
|
||||||
|
let dir_symlink_times = symlink_times(&dirs.test().join("ds"));
|
||||||
|
let dir_file_symlink_times = symlink_times(&dirs.test().join("fds"));
|
||||||
|
let file_missing_symlink_times = symlink_times(&dirs.test().join("fms"));
|
||||||
|
|
||||||
|
assert_eq!(file_symlink_times.1, TIME_ONE);
|
||||||
|
assert_eq!(dir_symlink_times.1, TIME_ONE);
|
||||||
|
assert_eq!(dir_file_symlink_times.1, TIME_ONE);
|
||||||
|
assert_eq!(file_missing_symlink_times.1, TIME_ONE);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_follow_symlinks() {
|
||||||
|
Playground::setup("touch_touches_symlinks", |dirs, sandbox| {
|
||||||
|
setup_symlink_fs(&dirs, sandbox);
|
||||||
|
|
||||||
|
let missing = dirs.test().join("m");
|
||||||
|
assert!(!missing.exists());
|
||||||
|
|
||||||
|
nu!(
|
||||||
|
cwd: dirs.test(),
|
||||||
|
"
|
||||||
|
touch fds -s
|
||||||
|
touch ds -s
|
||||||
|
touch fs -s
|
||||||
|
touch fms -s
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// We did not create the missing symlink target
|
||||||
|
assert!(!missing.exists());
|
||||||
|
|
||||||
|
// The timestamps for files and directories remain the same
|
||||||
|
let file_times = symlink_times(&dirs.test().join("f"));
|
||||||
|
let dir_times = symlink_times(&dirs.test().join("d"));
|
||||||
|
let dir_file_times = symlink_times(&dirs.test().join("d/f"));
|
||||||
|
|
||||||
|
assert_eq!(file_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_eq!(dir_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_eq!(dir_file_times, (TIME_ONE, TIME_ONE));
|
||||||
|
|
||||||
|
// For symlinks, everything changed. (except their targets, and paths, and personality)
|
||||||
|
let file_symlink_times = symlink_times(&dirs.test().join("fs"));
|
||||||
|
let dir_symlink_times = symlink_times(&dirs.test().join("ds"));
|
||||||
|
let dir_file_symlink_times = symlink_times(&dirs.test().join("fds"));
|
||||||
|
let file_missing_symlink_times = symlink_times(&dirs.test().join("fms"));
|
||||||
|
|
||||||
|
assert_ne!(file_symlink_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_ne!(dir_symlink_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_ne!(dir_file_symlink_times, (TIME_ONE, TIME_ONE));
|
||||||
|
assert_ne!(file_missing_symlink_times, (TIME_ONE, TIME_ONE));
|
||||||
|
})
|
||||||
|
}
|
@ -3392,6 +3392,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
Arg,
|
Arg,
|
||||||
AfterCommaArg,
|
AfterCommaArg,
|
||||||
Type,
|
Type,
|
||||||
|
AfterType,
|
||||||
DefaultValue,
|
DefaultValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3425,7 +3426,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
let mut args: Vec<Arg> = vec![];
|
let mut args: Vec<Arg> = vec![];
|
||||||
let mut parse_mode = ParseMode::Arg;
|
let mut parse_mode = ParseMode::Arg;
|
||||||
|
|
||||||
for token in &output {
|
for (index, token) in output.iter().enumerate() {
|
||||||
|
let last_token = index == output.len() - 1;
|
||||||
|
|
||||||
match token {
|
match token {
|
||||||
Token {
|
Token {
|
||||||
contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator,
|
contents: crate::TokenContents::Item | crate::TokenContents::AssignmentOperator,
|
||||||
@ -3437,10 +3440,12 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
// The : symbol separates types
|
// The : symbol separates types
|
||||||
if contents == b":" {
|
if contents == b":" {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
|
ParseMode::Arg if last_token => working_set
|
||||||
|
.error(ParseError::Expected("type", Span::new(span.end, span.end))),
|
||||||
ParseMode::Arg => {
|
ParseMode::Arg => {
|
||||||
parse_mode = ParseMode::Type;
|
parse_mode = ParseMode::Type;
|
||||||
}
|
}
|
||||||
ParseMode::AfterCommaArg => {
|
ParseMode::AfterCommaArg | ParseMode::AfterType => {
|
||||||
working_set.error(ParseError::Expected("parameter or flag", span));
|
working_set.error(ParseError::Expected("parameter or flag", span));
|
||||||
}
|
}
|
||||||
ParseMode::Type | ParseMode::DefaultValue => {
|
ParseMode::Type | ParseMode::DefaultValue => {
|
||||||
@ -3452,9 +3457,15 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
// The = symbol separates a variable from its default value
|
// The = symbol separates a variable from its default value
|
||||||
else if contents == b"=" {
|
else if contents == b"=" {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
ParseMode::Type | ParseMode::Arg => {
|
ParseMode::Arg | ParseMode::AfterType if last_token => working_set.error(
|
||||||
|
ParseError::Expected("default value", Span::new(span.end, span.end)),
|
||||||
|
),
|
||||||
|
ParseMode::Arg | ParseMode::AfterType => {
|
||||||
parse_mode = ParseMode::DefaultValue;
|
parse_mode = ParseMode::DefaultValue;
|
||||||
}
|
}
|
||||||
|
ParseMode::Type => {
|
||||||
|
working_set.error(ParseError::Expected("type", span));
|
||||||
|
}
|
||||||
ParseMode::AfterCommaArg => {
|
ParseMode::AfterCommaArg => {
|
||||||
working_set.error(ParseError::Expected("parameter or flag", span));
|
working_set.error(ParseError::Expected("parameter or flag", span));
|
||||||
}
|
}
|
||||||
@ -3467,7 +3478,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
// The , symbol separates params only
|
// The , symbol separates params only
|
||||||
else if contents == b"," {
|
else if contents == b"," {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
ParseMode::Arg => parse_mode = ParseMode::AfterCommaArg,
|
ParseMode::Arg | ParseMode::AfterType => {
|
||||||
|
parse_mode = ParseMode::AfterCommaArg
|
||||||
|
}
|
||||||
ParseMode::AfterCommaArg => {
|
ParseMode::AfterCommaArg => {
|
||||||
working_set.error(ParseError::Expected("parameter or flag", span));
|
working_set.error(ParseError::Expected("parameter or flag", span));
|
||||||
}
|
}
|
||||||
@ -3480,7 +3493,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match parse_mode {
|
match parse_mode {
|
||||||
ParseMode::Arg | ParseMode::AfterCommaArg => {
|
ParseMode::Arg | ParseMode::AfterCommaArg | ParseMode::AfterType => {
|
||||||
// Long flag with optional short form following with no whitespace, e.g. --output, --age(-a)
|
// Long flag with optional short form following with no whitespace, e.g. --output, --age(-a)
|
||||||
if contents.starts_with(b"--") && contents.len() > 2 {
|
if contents.starts_with(b"--") && contents.len() > 2 {
|
||||||
// Split the long flag from the short flag with the ( character as delimiter.
|
// Split the long flag from the short flag with the ( character as delimiter.
|
||||||
@ -3790,7 +3803,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parse_mode = ParseMode::Arg;
|
parse_mode = ParseMode::AfterType;
|
||||||
}
|
}
|
||||||
ParseMode::DefaultValue => {
|
ParseMode::DefaultValue => {
|
||||||
if let Some(last) = args.last_mut() {
|
if let Some(last) = args.last_mut() {
|
||||||
|
@ -34,7 +34,7 @@ impl PluginCommand for ExprAggGroups {
|
|||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
vec![Example {
|
vec![Example {
|
||||||
description: "Get the groiup index of the group by operations.",
|
description: "Get the group index of the group by operations.",
|
||||||
example: r#"[[group value]; [one 94] [one 95] [one 96] [two 97] [two 98] [two 99]]
|
example: r#"[[group value]; [one 94] [one 95] [one 96] [two 97] [two 98] [two 99]]
|
||||||
| polars into-df
|
| polars into-df
|
||||||
| polars group-by group
|
| polars group-by group
|
||||||
|
@ -28,7 +28,7 @@ impl PluginCommand for ValueCount {
|
|||||||
.named(
|
.named(
|
||||||
"column",
|
"column",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
"Provide a custom name for the coutn column",
|
"Provide a custom name for the count column",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
.switch("sort", "Whether or not values should be sorted", Some('s'))
|
.switch("sort", "Whether or not values should be sorted", Some('s'))
|
||||||
|
Loading…
Reference in New Issue
Block a user