diff --git a/Cargo.lock b/Cargo.lock index a00e672645..c68df03732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3229,6 +3229,7 @@ dependencies = [ "uu_mkdir", "uu_mktemp", "uu_mv", + "uu_touch", "uu_uname", "uu_whoami", "uucore", @@ -4084,6 +4085,17 @@ dependencies = [ "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]] name = "paste" version = "1.0.15" @@ -6700,9 +6712,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uu_cp" -version = "0.0.27" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb99d355ccb02e8c514e4a1d93e4aa4eedea9837de24635dfd24c165971444e" +checksum = "e0eff79f5eacf6bb88c9afc19f3cec2ab14ad31317be1369100658b46d41e410" dependencies = [ "clap", "filetime", @@ -6716,9 +6728,9 @@ dependencies = [ [[package]] name = "uu_mkdir" -version = "0.0.27" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219588fbc146f18188781208ac4034616c51cf151677b4e1f9caf63ca8a7f2cf" +checksum = "feba7cf875eecbb746b1c5a5a8a031ab3a00e5f44f5441643a06b78577780d3a" dependencies = [ "clap", "uucore", @@ -6726,9 +6738,9 @@ dependencies = [ [[package]] name = "uu_mktemp" -version = "0.0.27" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e79ad2c5911908fce23a6069c52ca82e1997e2ed4bf6abf2d867c79c3dc73f" +checksum = "1a9cfd389f60e667c5ee6659beaad50bada7e710d76082c7d77ab91e04307c8f" dependencies = [ "clap", "rand", @@ -6738,9 +6750,9 @@ dependencies = [ [[package]] name = "uu_mv" -version = "0.0.27" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd57c8d02f8a99ed56ed9f6fddab403ee0e2bf9e8f3a5ca8f0f9e4d6e3e392a0" +checksum = "bf932231fccdf108f75443bab0ce17acfe49b5825d731b8a358251833be7da20" dependencies = [ "clap", "fs_extra", @@ -6749,10 +6761,24 @@ dependencies = [ ] [[package]] -name = "uu_uname" -version = "0.0.27" +name = "uu_touch" +version = "0.0.28" 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 = [ "clap", "platform-info", @@ -6761,27 +6787,27 @@ dependencies = [ [[package]] name = "uu_whoami" -version = "0.0.27" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7c52e42e0425710461700adc1063f468f2ba8a8ff83ee69ba661095ab7b77a" +checksum = "5d15200414428c65f95d0b1d1226fc84f74ae80376bfe59959d93ddf57f944f5" dependencies = [ "clap", "libc", "uucore", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "uucore" -version = "0.0.27" +version = "0.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b54aad02cf7e96f5fafabb6b836efa73eef934783b17530095a29ffd4fdc154" +checksum = "04ea43050c46912575654c5181f4135529e8d4003fca80803af10cdef3ca6412" dependencies = [ "clap", "dunce", "glob", "libc", - "nix 0.28.0", + "nix 0.29.0", "number_prefix", "once_cell", "os_display", @@ -6789,7 +6815,7 @@ dependencies = [ "walkdir", "wild", "winapi-util", - "windows-sys 0.48.0", + "windows-sys 0.59.0", "xattr", ] diff --git a/Cargo.toml b/Cargo.toml index 394565702e..f37288fac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,13 +165,14 @@ unicode-segmentation = "1.12" unicode-width = "0.1" ureq = { version = "2.10", default-features = false } url = "2.2" -uu_cp = "0.0.27" -uu_mkdir = "0.0.27" -uu_mktemp = "0.0.27" -uu_mv = "0.0.27" -uu_whoami = "0.0.27" -uu_uname = "0.0.27" -uucore = "0.0.27" +uu_cp = "0.0.28" +uu_mkdir = "0.0.28" +uu_mktemp = "0.0.28" +uu_mv = "0.0.28" +uu_touch = "0.0.28" +uu_whoami = "0.0.28" +uu_uname = "0.0.28" +uucore = "0.0.28" uuid = "1.11.0" v_htmlescape = "0.15.0" wax = "0.6" diff --git a/crates/nu-cli/src/prompt_update.rs b/crates/nu-cli/src/prompt_update.rs index 63dd9ce19d..c033475586 100644 --- a/crates/nu-cli/src/prompt_update.rs +++ b/crates/nu-cli/src/prompt_update.rs @@ -1,5 +1,5 @@ use crate::NushellPrompt; -use log::trace; +use log::{trace, warn}; use nu_engine::ClosureEvalOnce; use nu_protocol::{ engine::{EngineState, Stack}, @@ -80,8 +80,13 @@ fn get_prompt_string( }) .and_then(|pipeline_data| { 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. if x.ends_with('\n') { x.pop(); @@ -91,7 +96,11 @@ fn get_prompt_string( x.pop(); } x - }) + }); + // Let's keep this for debugging purposes with nu --log-level warn + warn!("{}:{}:{} {:?}", file!(), line!(), column!(), ansi_output); + + ansi_output }) } diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index e9c3b9a784..c37954e9c1 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -96,6 +96,7 @@ uu_cp = { workspace = true } uu_mkdir = { workspace = true } uu_mktemp = { workspace = true } uu_mv = { workspace = true } +uu_touch = { workspace = true } uu_uname = { workspace = true } uu_whoami = { workspace = true } uuid = { workspace = true, features = ["v4"] } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ac6fb46631..cbbee717d4 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -230,6 +230,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Rm, Save, Touch, + UTouch, Glob, Watch, }; diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index acfa54fee3..089e899dda 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -12,6 +12,7 @@ mod ucp; mod umkdir; mod umv; mod util; +mod utouch; mod watch; pub use self::open::Open; @@ -27,4 +28,5 @@ pub use touch::Touch; pub use ucp::UCp; pub use umkdir::UMkdir; pub use umv::UMv; +pub use utouch::UTouch; pub use watch::Watch; diff --git a/crates/nu-command/src/filesystem/umv.rs b/crates/nu-command/src/filesystem/umv.rs index 0f49a7d97d..929fc7bcbb 100644 --- a/crates/nu-command/src/filesystem/umv.rs +++ b/crates/nu-command/src/filesystem/umv.rs @@ -188,6 +188,7 @@ impl Command for UMv { target_dir: None, no_target_dir: false, strip_slashes: false, + debug: false, }; if let Err(error) = uu_mv::mv(&files, &options) { return Err(ShellError::GenericError { diff --git a/crates/nu-command/src/filesystem/utouch.rs b/crates/nu-command/src/filesystem/utouch.rs new file mode 100644 index 0000000000..f32364b28a --- /dev/null +++ b/crates/nu-command/src/filesystem/utouch.rs @@ -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 { + 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> = + 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::>(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::>(engine_state, stack, "date")? { + (Some(date.item), Some(date.span)) + } else { + (None, None) + }; + let timestamp: Option>> = + 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 { + 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, + }, + ] + } +} diff --git a/crates/nu-command/src/filters/group_by.rs b/crates/nu-command/src/filters/group_by.rs index 9396de4781..0f56152834 100644 --- a/crates/nu-command/src/filters/group_by.rs +++ b/crates/nu-command/src/filters/group_by.rs @@ -1,6 +1,6 @@ use indexmap::IndexMap; use nu_engine::{command_prelude::*, ClosureEval}; -use nu_protocol::{engine::Closure, IntoValue}; +use nu_protocol::{engine::Closure, FromValue, IntoValue}; #[derive(Clone)] pub struct GroupBy; @@ -12,10 +12,6 @@ impl Command for GroupBy { fn signature(&self) -> Signature { 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)]) .switch( "to-table", @@ -229,7 +225,7 @@ pub fn group_by( input: PipelineData, ) -> Result { let head = call.head; - let groupers: Vec = call.rest(engine_state, stack, 0)?; + let groupers: Vec> = call.rest(engine_state, stack, 0)?; let to_table = call.has_flag(engine_state, stack, "to-table")?; let config = engine_state.get_config(); @@ -238,20 +234,20 @@ pub fn group_by( return Ok(Value::record(Record::new(), head).into_pipeline_data()); } - let mut groupers = groupers.into_iter(); - - let grouped = if let Some(grouper) = groupers.next() { - let mut groups = Grouped::new(&grouper, values, config, engine_state, stack)?; - for grouper in groupers { - groups.subgroup(&grouper, config, engine_state, stack)?; + let grouped = match &groupers[..] { + [first, rest @ ..] => { + let mut grouped = Grouped::new(first.as_ref(), values, config, engine_state, stack)?; + for grouper in rest { + grouped.subgroup(grouper.as_ref(), config, engine_state, stack)?; + } + grouped } - groups - } else { - Grouped::empty(values, config) + [] => Grouped::empty(values, config), }; let value = if to_table { - grouped.into_table(head) + let column_names = groupers_to_column_names(&groupers)?; + grouped.into_table(&column_names, head) } else { grouped.into_record(head) }; @@ -259,8 +255,67 @@ pub fn group_by( Ok(value.into_pipeline_data()) } +fn groupers_to_column_names(groupers: &[Spanned]) -> Result, 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> = 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 = name_set + .into_iter() + .map(|elem| elem.item) + .chain(["items".into()]) + .collect(); + Ok(column_names) +} + fn group_cell_path( - column_name: CellPath, + column_name: &CellPath, values: Vec, config: &nu_protocol::Config, ) -> Result>, ShellError> { @@ -305,8 +360,25 @@ fn group_closure( Ok(groups) } +enum Grouper { + CellPath { val: CellPath }, + Closure { val: Box }, +} + +impl FromValue for Grouper { + fn from_value(v: Value) -> Result { + 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 { - grouper: Option, groups: Tree, } @@ -325,41 +397,35 @@ impl Grouped { } Self { - grouper: Some("group".into()), groups: Tree::Leaf(groups), } } fn new( - grouper: &Value, + grouper: Spanned<&Grouper>, values: Vec, config: &nu_protocol::Config, engine_state: &EngineState, stack: &mut Stack, ) -> Result { - let span = grouper.span(); - let groups = match grouper { - Value::CellPath { val, .. } => group_cell_path(val.clone(), values, config)?, - Value::Closure { val, .. } => { - group_closure(values, span, Closure::clone(val), engine_state, stack)? - } - _ => { - return Err(ShellError::TypeMismatch { - err_message: "unsupported grouper type".to_string(), - span, - }) - } + let groups = match grouper.item { + Grouper::CellPath { val } => group_cell_path(val, values, config)?, + Grouper::Closure { val } => group_closure( + values, + grouper.span, + Closure::clone(val), + engine_state, + stack, + )?, }; - let grouper = grouper.as_cell_path().ok().map(CellPath::to_column_name); Ok(Self { - grouper, groups: Tree::Leaf(groups), }) } fn subgroup( &mut self, - grouper: &Value, + grouper: Spanned<&Grouper>, config: &nu_protocol::Config, engine_state: &EngineState, stack: &mut Stack, @@ -384,34 +450,33 @@ impl Grouped { Ok(()) } - fn into_table(self, head: Span) -> Value { - self._into_table(head, 0) + fn into_table(self, column_names: &[String], head: Span) -> Value { + self._into_table(head) .into_iter() - .map(|row| row.into_iter().rev().collect::().into_value(head)) + .map(|row| { + row.into_iter() + .rev() + .zip(column_names) + .map(|(val, key)| (key.clone(), val)) + .collect::() + .into_value(head) + }) .collect::>() .into_value(head) } - fn _into_table(self, head: Span, index: usize) -> Vec { - let grouper = self.grouper.unwrap_or_else(|| format!("group{index}")); + fn _into_table(self, head: Span) -> Vec> { match self.groups { Tree::Leaf(leaf) => leaf .into_iter() - .map(|(group, values)| { - [ - ("items".to_string(), values.into_value(head)), - (grouper.clone(), group.into_value(head)), - ] - .into_iter() - .collect() - }) - .collect::>(), + .map(|(group, values)| vec![(values.into_value(head)), (group.into_value(head))]) + .collect::>>(), Tree::Branch(branch) => branch .into_iter() .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 { - row.insert(grouper.clone(), group.clone().into_value(head)); + row.push(group.clone().into_value(head)); } inner }) diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 67b848915d..604278c676 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -76,7 +76,7 @@ impl Command for External { // 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. 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()) { let ext = executable .extension() diff --git a/crates/nu-command/tests/commands/def.rs b/crates/nu-command/tests/commands/def.rs index c66869b479..0705c0085f 100644 --- a/crates/nu-command/tests/commands/def.rs +++ b/crates/nu-command/tests/commands/def.rs @@ -2,6 +2,13 @@ use nu_test_support::nu; use nu_test_support::playground::Playground; 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] fn def_with_comment() { Playground::setup("def_with_comment", |dirs, _| { @@ -72,6 +79,13 @@ fn def_errors_with_comma_before_equals() { 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] fn def_errors_with_comma_before_colon() { let actual = nu!("def test-command [ foo, : int ] {}"); @@ -85,7 +99,6 @@ fn def_errors_with_multiple_colons() { assert!(actual.err.contains("expected type")); } -#[ignore = "This error condition is not implemented yet"] #[test] fn def_errors_with_multiple_types() { 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")); } +#[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] fn def_errors_with_multiple_commas() { let actual = nu!("def test-command [ foo,,bar ] {}"); diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 63911ebfbc..678b8d8896 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -127,6 +127,7 @@ mod update; mod upsert; mod url; mod use_; +mod utouch; mod where_; mod which; mod while_; diff --git a/crates/nu-command/tests/commands/move_/umv.rs b/crates/nu-command/tests/commands/move_/umv.rs index b193d5b7cb..98e2a753bf 100644 --- a/crates/nu-command/tests/commands/move_/umv.rs +++ b/crates/nu-command/tests/commands/move_/umv.rs @@ -513,13 +513,18 @@ fn test_mv_no_clobber() { sandbox.with_files(&[EmptyFile(file_a)]); sandbox.with_files(&[EmptyFile(file_b)]); - let actual = nu!( + let _ = nu!( cwd: dirs.test(), "mv -n {} {}", file_a, 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"); }) } diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index eb2363e68c..e3fa2ca931 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -841,14 +841,13 @@ fn test_cp_arg_no_clobber() { let target = dirs.fixtures.join("cp").join(TEST_HOW_ARE_YOU_SOURCE); let target_hash = get_file_hash(target.display()); - let actual = nu!( - cwd: dirs.root(), - "cp {} {} --no-clobber", - src.display(), - target.display() + let _ = nu!( + cwd: dirs.root(), + "cp {} {} --no-clobber", + src.display(), + target.display() ); let after_cp_hash = get_file_hash(target.display()); - assert!(actual.err.contains("not replacing")); // Check content was not clobbered assert_eq!(after_cp_hash, target_hash); }); diff --git a/crates/nu-command/tests/commands/utouch.rs b/crates/nu-command/tests/commands/utouch.rs new file mode 100644 index 0000000000..062ec7ddfc --- /dev/null +++ b/crates/nu-command/tests/commands/utouch.rs @@ -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) -> (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::::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::::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::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::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::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::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::::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::::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::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::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::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::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)); + }) +} diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index e4d3765e17..7db2e112bf 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -3392,6 +3392,7 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) -> Arg, AfterCommaArg, Type, + AfterType, DefaultValue, } @@ -3425,7 +3426,9 @@ pub fn parse_signature_helper(working_set: &mut StateWorkingSet, span: Span) -> let mut args: Vec = vec![]; 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 { Token { 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 if contents == b":" { match parse_mode { + ParseMode::Arg if last_token => working_set + .error(ParseError::Expected("type", Span::new(span.end, span.end))), ParseMode::Arg => { parse_mode = ParseMode::Type; } - ParseMode::AfterCommaArg => { + ParseMode::AfterCommaArg | ParseMode::AfterType => { working_set.error(ParseError::Expected("parameter or flag", span)); } 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 else if contents == b"=" { 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; } + ParseMode::Type => { + working_set.error(ParseError::Expected("type", span)); + } ParseMode::AfterCommaArg => { 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 else if contents == b"," { match parse_mode { - ParseMode::Arg => parse_mode = ParseMode::AfterCommaArg, + ParseMode::Arg | ParseMode::AfterType => { + parse_mode = ParseMode::AfterCommaArg + } ParseMode::AfterCommaArg => { 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 { 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) if contents.starts_with(b"--") && contents.len() > 2 { // 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 => { if let Some(last) = args.last_mut() { diff --git a/crates/nu_plugin_polars/src/dataframe/command/aggregation/agg_groups.rs b/crates/nu_plugin_polars/src/dataframe/command/aggregation/agg_groups.rs index ae12239800..a8d58bd6f0 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/aggregation/agg_groups.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/aggregation/agg_groups.rs @@ -34,7 +34,7 @@ impl PluginCommand for ExprAggGroups { fn examples(&self) -> Vec { 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]] | polars into-df | polars group-by group diff --git a/crates/nu_plugin_polars/src/dataframe/command/aggregation/value_counts.rs b/crates/nu_plugin_polars/src/dataframe/command/aggregation/value_counts.rs index 064e308e80..57f7f3381a 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/aggregation/value_counts.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/aggregation/value_counts.rs @@ -28,7 +28,7 @@ impl PluginCommand for ValueCount { .named( "column", SyntaxShape::String, - "Provide a custom name for the coutn column", + "Provide a custom name for the count column", Some('c'), ) .switch("sort", "Whether or not values should be sorted", Some('s'))