Merge branch 'main' of github.com:PegasusPlusUS/nushell

This commit is contained in:
pegasus.cadence@gmail.com 2024-11-18 20:42:56 -08:00
commit dd3adbff89
18 changed files with 1256 additions and 97 deletions

62
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

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

View File

@ -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"] }

View File

@ -230,6 +230,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
Rm,
Save,
Touch,
UTouch,
Glob,
Watch,
};

View File

@ -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;

View File

@ -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 {

View 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,
},
]
}
}

View File

@ -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<PipelineData, ShellError> {
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 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<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(
column_name: CellPath,
column_name: &CellPath,
values: Vec<Value>,
config: &nu_protocol::Config,
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
@ -305,8 +360,25 @@ fn group_closure(
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 {
grouper: Option<String>,
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<Value>,
config: &nu_protocol::Config,
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<Self, ShellError> {
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::<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<_>>()
.into_value(head)
}
fn _into_table(self, head: Span, index: usize) -> Vec<Record> {
let grouper = self.grouper.unwrap_or_else(|| format!("group{index}"));
fn _into_table(self, head: Span) -> Vec<Vec<Value>> {
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::<Vec<Record>>(),
.map(|(group, values)| vec![(values.into_value(head)), (group.into_value(head))])
.collect::<Vec<Vec<Value>>>(),
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
})

View File

@ -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()

View File

@ -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 ] {}");

View File

@ -127,6 +127,7 @@ mod update;
mod upsert;
mod url;
mod use_;
mod utouch;
mod where_;
mod which;
mod while_;

View File

@ -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");
})
}

View File

@ -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);
});

View 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));
})
}

View File

@ -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<Arg> = 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() {

View File

@ -34,7 +34,7 @@ impl PluginCommand for ExprAggGroups {
fn examples(&self) -> 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]]
| polars into-df
| polars group-by group

View File

@ -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'))