mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
Merge branch 'nushell:main' into continue-PWD-per-drive-quote
This commit is contained in:
commit
9078c182e8
32 changed files with 457 additions and 89 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -5830,9 +5830,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
|
|
|
@ -143,7 +143,7 @@ regex = "1.9.5"
|
|||
rmp = "0.8"
|
||||
rmp-serde = "1.3"
|
||||
ropey = "1.6.1"
|
||||
roxmltree = "0.19"
|
||||
roxmltree = "0.20"
|
||||
rstest = { version = "0.23", default-features = false }
|
||||
rusqlite = "0.31"
|
||||
rust-embed = "8.5.0"
|
||||
|
|
|
@ -2,4 +2,4 @@ mod from;
|
|||
mod to;
|
||||
|
||||
pub(crate) use from::url::FromUrl;
|
||||
pub(crate) use to::html::ToHtml;
|
||||
pub use to::html::ToHtml;
|
||||
|
|
|
@ -9,6 +9,7 @@ mod strings;
|
|||
pub use bits::{
|
||||
Bits, BitsAnd, BitsInto, BitsNot, BitsOr, BitsRol, BitsRor, BitsShl, BitsShr, BitsXor,
|
||||
};
|
||||
pub use formats::ToHtml;
|
||||
pub use math::{MathArcCos, MathArcCosH, MathArcSin, MathArcSinH, MathArcTan, MathArcTanH};
|
||||
pub use math::{MathCos, MathCosH, MathSin, MathSinH, MathTan, MathTanH};
|
||||
pub use math::{MathExp, MathLn};
|
||||
|
@ -54,7 +55,8 @@ pub fn add_extra_command_context(mut engine_state: EngineState) -> EngineState {
|
|||
strings::str_::case::StrTitleCase
|
||||
);
|
||||
|
||||
bind_command!(formats::ToHtml, formats::FromUrl);
|
||||
bind_command!(ToHtml, formats::FromUrl);
|
||||
|
||||
// Bits
|
||||
bind_command! {
|
||||
Bits,
|
||||
|
|
|
@ -106,6 +106,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||
bind_command! {
|
||||
Path,
|
||||
PathBasename,
|
||||
PathSelf,
|
||||
PathDirname,
|
||||
PathExists,
|
||||
PathExpand,
|
||||
|
|
|
@ -15,7 +15,7 @@ impl Command for UTouch {
|
|||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["create", "file"]
|
||||
vec!["create", "file", "coreutils"]
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
|
|
|
@ -6,6 +6,7 @@ mod join;
|
|||
mod parse;
|
||||
pub mod path_;
|
||||
mod relative_to;
|
||||
mod self_;
|
||||
mod split;
|
||||
mod r#type;
|
||||
|
||||
|
@ -18,6 +19,7 @@ pub use parse::SubCommand as PathParse;
|
|||
pub use path_::PathCommand as Path;
|
||||
pub use r#type::SubCommand as PathType;
|
||||
pub use relative_to::SubCommand as PathRelativeTo;
|
||||
pub use self_::SubCommand as PathSelf;
|
||||
pub use split::SubCommand as PathSplit;
|
||||
|
||||
use nu_protocol::{ShellError, Span, Value};
|
||||
|
|
129
crates/nu-command/src/path/self_.rs
Normal file
129
crates/nu-command/src/path/self_.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use nu_engine::command_prelude::*;
|
||||
use nu_path::expand_path_with;
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SubCommand;
|
||||
|
||||
impl Command for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"path self"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("path self")
|
||||
.input_output_type(Type::Nothing, Type::String)
|
||||
.allow_variants_without_examples(true)
|
||||
.optional(
|
||||
"path",
|
||||
SyntaxShape::Filepath,
|
||||
"Path to get instead of the current file.",
|
||||
)
|
||||
.category(Category::Path)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Get the absolute path of the script or module containing this command at parse time."
|
||||
}
|
||||
|
||||
fn is_const(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Err(ShellError::GenericError {
|
||||
error: "this command can only run during parse-time".into(),
|
||||
msg: "can't run after parse-time".into(),
|
||||
span: Some(call.head),
|
||||
help: Some("try assigning this command's output to a const variable".into()),
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn run_const(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let path: Option<String> = call.opt_const(working_set, 0)?;
|
||||
let cwd = working_set.permanent_state.cwd(None)?;
|
||||
let current_file =
|
||||
working_set
|
||||
.files
|
||||
.top()
|
||||
.ok_or_else(|| ShellError::FileNotFoundCustom {
|
||||
msg: "Couldn't find current file".into(),
|
||||
span: call.head,
|
||||
})?;
|
||||
|
||||
let out = if let Some(path) = path {
|
||||
let dir = expand_path_with(
|
||||
current_file
|
||||
.parent()
|
||||
.ok_or_else(|| ShellError::FileNotFoundCustom {
|
||||
msg: "Couldn't find current file's parent.".into(),
|
||||
span: call.head,
|
||||
})?,
|
||||
&cwd,
|
||||
true,
|
||||
);
|
||||
Value::string(
|
||||
expand_path_with(path, dir, false).to_string_lossy(),
|
||||
call.head,
|
||||
)
|
||||
} else {
|
||||
Value::string(
|
||||
expand_path_with(current_file, &cwd, true).to_string_lossy(),
|
||||
call.head,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(out.into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get the path of the current file",
|
||||
example: r#"const current_file = path self"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Get the path of the directory containing the current file",
|
||||
example: r#"const current_file = path self ."#,
|
||||
result: None,
|
||||
},
|
||||
#[cfg(windows)]
|
||||
Example {
|
||||
description: "Get the absolute form of a path relative to the current file",
|
||||
example: r#"const current_file = path self ..\foo"#,
|
||||
result: None,
|
||||
},
|
||||
#[cfg(not(windows))]
|
||||
Example {
|
||||
description: "Get the absolute form of a path relative to the current file",
|
||||
example: r#"const current_file = path self ../foo"#,
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
}
|
|
@ -70,8 +70,8 @@ impl Command for Sleep {
|
|||
result: Some(Value::nothing(Span::test_data())),
|
||||
},
|
||||
Example {
|
||||
description: "Sleep for 3sec",
|
||||
example: "sleep 1sec 1sec 1sec",
|
||||
description: "Use multiple arguments to write a duration with multiple units, which is unsupported by duration literals",
|
||||
example: "sleep 1min 30sec",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
|
|
|
@ -131,14 +131,21 @@ fn http_delete_timeout() {
|
|||
let _mock = server
|
||||
.mock("DELETE", "/")
|
||||
.with_chunked_body(|w| {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
w.write_all(b"Delayed response!")
|
||||
})
|
||||
.create();
|
||||
|
||||
let actual = nu!(pipeline(
|
||||
format!("http delete --max-time 500ms {url}", url = server.url()).as_str()
|
||||
format!("http delete --max-time 100ms {url}", url = server.url()).as_str()
|
||||
));
|
||||
|
||||
assert!(&actual.err.contains("nu::shell::io_error"));
|
||||
assert!(&actual.err.contains("nu::shell::network_failure"));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(&actual.err.contains("timed out reading response"));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(&actual
|
||||
.err
|
||||
.contains("did not properly respond after a period of time"));
|
||||
}
|
||||
|
|
|
@ -325,14 +325,21 @@ fn http_get_timeout() {
|
|||
let _mock = server
|
||||
.mock("GET", "/")
|
||||
.with_chunked_body(|w| {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
w.write_all(b"Delayed response!")
|
||||
})
|
||||
.create();
|
||||
|
||||
let actual = nu!(pipeline(
|
||||
format!("http get --max-time 500ms {url}", url = server.url()).as_str()
|
||||
format!("http get --max-time 100ms {url}", url = server.url()).as_str()
|
||||
));
|
||||
|
||||
assert!(&actual.err.contains("nu::shell::io_error"));
|
||||
assert!(&actual.err.contains("nu::shell::network_failure"));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(&actual.err.contains("timed out reading response"));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(&actual
|
||||
.err
|
||||
.contains("did not properly respond after a period of time"));
|
||||
}
|
||||
|
|
|
@ -50,14 +50,21 @@ fn http_options_timeout() {
|
|||
let _mock = server
|
||||
.mock("OPTIONS", "/")
|
||||
.with_chunked_body(|w| {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
w.write_all(b"Delayed response!")
|
||||
})
|
||||
.create();
|
||||
|
||||
let actual = nu!(pipeline(
|
||||
format!("http options --max-time 500ms {url}", url = server.url()).as_str()
|
||||
format!("http options --max-time 100ms {url}", url = server.url()).as_str()
|
||||
));
|
||||
|
||||
assert!(&actual.err.contains("nu::shell::io_error"));
|
||||
assert!(&actual.err.contains("nu::shell::network_failure"));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(&actual.err.contains("timed out reading response"));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(&actual
|
||||
.err
|
||||
.contains("did not properly respond after a period of time"));
|
||||
}
|
||||
|
|
|
@ -171,18 +171,25 @@ fn http_patch_timeout() {
|
|||
let _mock = server
|
||||
.mock("PATCH", "/")
|
||||
.with_chunked_body(|w| {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
w.write_all(b"Delayed response!")
|
||||
})
|
||||
.create();
|
||||
|
||||
let actual = nu!(pipeline(
|
||||
format!(
|
||||
"http patch --max-time 500ms {url} patchbody",
|
||||
"http patch --max-time 100ms {url} patchbody",
|
||||
url = server.url()
|
||||
)
|
||||
.as_str()
|
||||
));
|
||||
|
||||
assert!(&actual.err.contains("nu::shell::io_error"));
|
||||
assert!(&actual.err.contains("nu::shell::network_failure"));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(&actual.err.contains("timed out reading response"));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(&actual
|
||||
.err
|
||||
.contains("did not properly respond after a period of time"));
|
||||
}
|
||||
|
|
|
@ -285,18 +285,25 @@ fn http_post_timeout() {
|
|||
let _mock = server
|
||||
.mock("POST", "/")
|
||||
.with_chunked_body(|w| {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
w.write_all(b"Delayed response!")
|
||||
})
|
||||
.create();
|
||||
|
||||
let actual = nu!(pipeline(
|
||||
format!(
|
||||
"http post --max-time 500ms {url} postbody",
|
||||
"http post --max-time 100ms {url} postbody",
|
||||
url = server.url()
|
||||
)
|
||||
.as_str()
|
||||
));
|
||||
|
||||
assert!(&actual.err.contains("nu::shell::io_error"));
|
||||
assert!(&actual.err.contains("nu::shell::network_failure"));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(&actual.err.contains("timed out reading response"));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(&actual
|
||||
.err
|
||||
.contains("did not properly respond after a period of time"));
|
||||
}
|
||||
|
|
|
@ -171,18 +171,25 @@ fn http_put_timeout() {
|
|||
let _mock = server
|
||||
.mock("PUT", "/")
|
||||
.with_chunked_body(|w| {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
w.write_all(b"Delayed response!")
|
||||
})
|
||||
.create();
|
||||
|
||||
let actual = nu!(pipeline(
|
||||
format!(
|
||||
"http put --max-time 500ms {url} putbody",
|
||||
"http put --max-time 100ms {url} putbody",
|
||||
url = server.url()
|
||||
)
|
||||
.as_str()
|
||||
));
|
||||
|
||||
assert!(&actual.err.contains("nu::shell::io_error"));
|
||||
assert!(&actual.err.contains("nu::shell::network_failure"));
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
assert!(&actual.err.contains("timed out reading response"));
|
||||
#[cfg(target_os = "windows")]
|
||||
assert!(&actual
|
||||
.err
|
||||
.contains("did not properly respond after a period of time"));
|
||||
}
|
||||
|
|
|
@ -284,9 +284,9 @@ fn use_main_def_known_external() {
|
|||
#[test]
|
||||
fn use_main_not_exported() {
|
||||
let inp = &[
|
||||
r#"module spam { def main [] { "spam" } }"#,
|
||||
r#"use spam"#,
|
||||
r#"spam"#,
|
||||
r#"module my-super-cool-and-unique-module-name { def main [] { "hi" } }"#,
|
||||
r#"use my-super-cool-and-unique-module-name"#,
|
||||
r#"my-super-cool-and-unique-module-name"#,
|
||||
];
|
||||
|
||||
let actual = nu!(&inp.join("; "));
|
||||
|
|
|
@ -488,6 +488,7 @@ impl<'e, 's> ScopeData<'e, 's> {
|
|||
"description" => Value::string(module_desc, span),
|
||||
"extra_description" => Value::string(module_extra_desc, span),
|
||||
"module_id" => Value::int(module_id.get() as i64, span),
|
||||
"file" => Value::string(module.file.clone().map_or("unknown".to_string(), |(p, _)| p.path().to_string_lossy().to_string()), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ use miette::{IntoDiagnostic, Result};
|
|||
use nu_parser::parse;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, StateWorkingSet},
|
||||
Value,
|
||||
Span, Value,
|
||||
};
|
||||
|
||||
impl LanguageServer {
|
||||
|
@ -28,6 +28,7 @@ impl LanguageServer {
|
|||
|
||||
let contents = rope_of_file.bytes().collect::<Vec<u8>>();
|
||||
let offset = working_set.next_span_start();
|
||||
working_set.files.push(file_path.into(), Span::unknown())?;
|
||||
parse(
|
||||
&mut working_set,
|
||||
Some(&file_path.to_string_lossy()),
|
||||
|
|
|
@ -274,6 +274,9 @@ impl LanguageServer {
|
|||
|
||||
// TODO: think about passing down the rope into the working_set
|
||||
let contents = file.bytes().collect::<Vec<u8>>();
|
||||
let _ = working_set
|
||||
.files
|
||||
.push(file_path.as_ref().into(), Span::unknown());
|
||||
let block = parse(working_set, Some(&file_path), &contents, false);
|
||||
let flattened = flatten_block(working_set, &block);
|
||||
|
||||
|
|
|
@ -2122,7 +2122,21 @@ pub fn parse_variable_expr(working_set: &mut StateWorkingSet, span: Span) -> Exp
|
|||
String::from_utf8_lossy(contents).to_string()
|
||||
};
|
||||
|
||||
if let Some(id) = parse_variable(working_set, span) {
|
||||
let bytes = working_set.get_span_contents(span);
|
||||
let suggestion = || {
|
||||
DidYouMean::new(
|
||||
&working_set.list_variables(),
|
||||
working_set.get_span_contents(span),
|
||||
)
|
||||
};
|
||||
if !is_variable(bytes) {
|
||||
working_set.error(ParseError::ExpectedWithDidYouMean(
|
||||
"valid variable name",
|
||||
suggestion(),
|
||||
span,
|
||||
));
|
||||
garbage(working_set, span)
|
||||
} else if let Some(id) = working_set.find_variable(bytes) {
|
||||
Expression::new(
|
||||
working_set,
|
||||
Expr::Var(id),
|
||||
|
@ -2133,9 +2147,7 @@ pub fn parse_variable_expr(working_set: &mut StateWorkingSet, span: Span) -> Exp
|
|||
working_set.error(ParseError::EnvVarNotVar(name, span));
|
||||
garbage(working_set, span)
|
||||
} else {
|
||||
let ws = &*working_set;
|
||||
let suggestion = DidYouMean::new(&ws.list_variables(), ws.get_span_contents(span));
|
||||
working_set.error(ParseError::VariableNotFound(suggestion, span));
|
||||
working_set.error(ParseError::VariableNotFound(suggestion(), span));
|
||||
garbage(working_set, span)
|
||||
}
|
||||
}
|
||||
|
@ -3349,26 +3361,66 @@ pub fn parse_input_output_types(
|
|||
}
|
||||
|
||||
pub fn parse_full_signature(working_set: &mut StateWorkingSet, spans: &[Span]) -> Expression {
|
||||
let arg_signature = working_set.get_span_contents(spans[0]);
|
||||
|
||||
if arg_signature.ends_with(b":") {
|
||||
let mut arg_signature =
|
||||
parse_signature(working_set, Span::new(spans[0].start, spans[0].end - 1));
|
||||
|
||||
let input_output_types = parse_input_output_types(working_set, &spans[1..]);
|
||||
|
||||
if let Expression {
|
||||
expr: Expr::Signature(sig),
|
||||
span: expr_span,
|
||||
..
|
||||
} = &mut arg_signature
|
||||
{
|
||||
sig.input_output_types = input_output_types;
|
||||
expr_span.end = Span::concat(&spans[1..]).end;
|
||||
match spans.len() {
|
||||
// This case should never happen. It corresponds to declarations like `def foo {}`,
|
||||
// which should throw a 'Missing required positional argument.' before getting to this point
|
||||
0 => {
|
||||
working_set.error(ParseError::InternalError(
|
||||
"failed to catch missing positional arguments".to_string(),
|
||||
Span::concat(spans),
|
||||
));
|
||||
garbage(working_set, Span::concat(spans))
|
||||
}
|
||||
|
||||
// e.g. `[ b"[foo: string]" ]`
|
||||
1 => parse_signature(working_set, spans[0]),
|
||||
|
||||
// This case is needed to distinguish between e.g.
|
||||
// `[ b"[]", b"{ true }" ]` vs `[ b"[]:", b"int" ]`
|
||||
2 if working_set.get_span_contents(spans[1]).starts_with(b"{") => {
|
||||
parse_signature(working_set, spans[0])
|
||||
}
|
||||
|
||||
// This should handle every other case, e.g.
|
||||
// `[ b"[]:", b"int" ]`
|
||||
// `[ b"[]", b":", b"int" ]`
|
||||
// `[ b"[]", b":", b"int", b"->", b"bool" ]`
|
||||
_ => {
|
||||
let (mut arg_signature, input_output_types_pos) =
|
||||
if working_set.get_span_contents(spans[0]).ends_with(b":") {
|
||||
(
|
||||
parse_signature(working_set, Span::new(spans[0].start, spans[0].end - 1)),
|
||||
1,
|
||||
)
|
||||
} else if working_set.get_span_contents(spans[1]) == b":" {
|
||||
(parse_signature(working_set, spans[0]), 2)
|
||||
} else {
|
||||
// This should be an error case, but we call parse_signature anyway
|
||||
// so it can handle the various possible errors
|
||||
// e.g. `[ b"[]", b"int" ]` or `[
|
||||
working_set.error(ParseError::Expected(
|
||||
"colon (:) before type signature",
|
||||
Span::concat(&spans[1..]),
|
||||
));
|
||||
// (garbage(working_set, Span::concat(spans)), 1)
|
||||
|
||||
(parse_signature(working_set, spans[0]), 1)
|
||||
};
|
||||
|
||||
let input_output_types =
|
||||
parse_input_output_types(working_set, &spans[input_output_types_pos..]);
|
||||
|
||||
if let Expression {
|
||||
expr: Expr::Signature(sig),
|
||||
span: expr_span,
|
||||
..
|
||||
} = &mut arg_signature
|
||||
{
|
||||
sig.input_output_types = input_output_types;
|
||||
expr_span.end = Span::concat(&spans[input_output_types_pos..]).end;
|
||||
}
|
||||
arg_signature
|
||||
}
|
||||
arg_signature
|
||||
} else {
|
||||
parse_signature(working_set, spans[0])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5612,18 +5664,6 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex
|
|||
}
|
||||
}
|
||||
|
||||
pub fn parse_variable(working_set: &mut StateWorkingSet, span: Span) -> Option<VarId> {
|
||||
let bytes = working_set.get_span_contents(span);
|
||||
|
||||
if is_variable(bytes) {
|
||||
working_set.find_variable(bytes)
|
||||
} else {
|
||||
working_set.error(ParseError::Expected("valid variable name", span));
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_builtin_commands(
|
||||
working_set: &mut StateWorkingSet,
|
||||
lite_command: &LiteCommand,
|
||||
|
|
|
@ -2460,6 +2460,7 @@ mod input_types {
|
|||
|
||||
#[rstest]
|
||||
#[case::input_output(b"def q []: int -> int {1}", false)]
|
||||
#[case::input_output(b"def q [x: bool]: int -> int {2}", false)]
|
||||
#[case::input_output(b"def q []: string -> string {'qwe'}", false)]
|
||||
#[case::input_output(b"def q []: nothing -> nothing {null}", false)]
|
||||
#[case::input_output(b"def q []: list<string> -> list<string> {[]}", false)]
|
||||
|
@ -2479,6 +2480,42 @@ mod input_types {
|
|||
#[case::input_output(b"def q []: nothing -> record<c: int e: int {{c: 1 e: 1}}", true)]
|
||||
#[case::input_output(b"def q []: record<c: int e: int -> record<a: int> {{a: 1}}", true)]
|
||||
#[case::input_output(b"def q []: nothing -> record<a: record<a: int> {{a: {a: 1}}}", true)]
|
||||
#[case::input_output(b"def q []: int []}", true)]
|
||||
#[case::input_output(b"def q []: bool {[]", true)]
|
||||
// Type signature variants with whitespace between inputs and `:`
|
||||
#[case::input_output(b"def q [] : int -> int {1}", false)]
|
||||
#[case::input_output(b"def q [x: bool] : int -> int {2}", false)]
|
||||
#[case::input_output(b"def q []\t : string -> string {'qwe'}", false)]
|
||||
#[case::input_output(b"def q [] \t : nothing -> nothing {null}", false)]
|
||||
#[case::input_output(b"def q [] \t: list<string> -> list<string> {[]}", false)]
|
||||
#[case::input_output(
|
||||
b"def q []\t: record<a: int b: int> -> record<c: int e: int> {{c: 1 e: 1}}",
|
||||
false
|
||||
)]
|
||||
#[case::input_output(
|
||||
b"def q [] : table<a: int b: int> -> table<c: int e: int> {[{c: 1 e: 1}]}",
|
||||
false
|
||||
)]
|
||||
#[case::input_output(
|
||||
b"def q [] : nothing -> record<c: record<a: int b: int> e: int> {{c: {a: 1 b: 2} e: 1}}",
|
||||
false
|
||||
)]
|
||||
#[case::input_output(b"def q [] : nothing -> list<string {[]}", true)]
|
||||
#[case::input_output(b"def q [] : nothing -> record<c: int e: int {{c: 1 e: 1}}", true)]
|
||||
#[case::input_output(b"def q [] : record<c: int e: int -> record<a: int> {{a: 1}}", true)]
|
||||
#[case::input_output(b"def q [] : nothing -> record<a: record<a: int> {{a: {a: 1}}}", true)]
|
||||
#[case::input_output(b"def q [] : int []}", true)]
|
||||
#[case::input_output(b"def q [] : bool {[]", true)]
|
||||
// No input-output type signature
|
||||
#[case::input_output(b"def qq [] {[]}", false)]
|
||||
#[case::input_output(b"def q [] []}", true)]
|
||||
#[case::input_output(b"def q [] {", true)]
|
||||
#[case::input_output(b"def q []: []}", true)]
|
||||
#[case::input_output(b"def q [] int {}", true)]
|
||||
#[case::input_output(b"def q [x: string, y: int] {{c: 1 e: 1}}", false)]
|
||||
#[case::input_output(b"def q [x: string, y: int]: {}", true)]
|
||||
#[case::input_output(b"def q [x: string, y: int] {a: {a: 1}}", true)]
|
||||
#[case::input_output(b"def foo {3}", true)]
|
||||
#[case::vardecl(b"let a: int = 1", false)]
|
||||
#[case::vardecl(b"let a: string = 'qwe'", false)]
|
||||
#[case::vardecl(b"let a: nothing = null", false)]
|
||||
|
|
|
@ -55,6 +55,10 @@ pub enum ParseError {
|
|||
#[diagnostic(code(nu::parser::parse_mismatch_with_full_string_msg))]
|
||||
ExpectedWithStringMsg(String, #[label("expected {0}")] Span),
|
||||
|
||||
#[error("Parse mismatch during operation.")]
|
||||
#[diagnostic(code(nu::parser::parse_mismatch_with_did_you_mean))]
|
||||
ExpectedWithDidYouMean(&'static str, DidYouMean, #[label("expected {0}. {1}")] Span),
|
||||
|
||||
#[error("Command does not support {0} input.")]
|
||||
#[diagnostic(code(nu::parser::input_type_mismatch))]
|
||||
InputMismatch(Type, #[label("command doesn't support {0} input")] Span),
|
||||
|
@ -551,6 +555,7 @@ impl ParseError {
|
|||
ParseError::Unbalanced(_, _, s) => *s,
|
||||
ParseError::Expected(_, s) => *s,
|
||||
ParseError::ExpectedWithStringMsg(_, s) => *s,
|
||||
ParseError::ExpectedWithDidYouMean(_, _, s) => *s,
|
||||
ParseError::Mismatch(_, _, s) => *s,
|
||||
ParseError::UnsupportedOperationLHS(_, _, s, _) => *s,
|
||||
ParseError::UnsupportedOperationRHS(_, _, _, _, s, _) => *s,
|
||||
|
|
|
@ -1220,10 +1220,10 @@ pub enum ShellError {
|
|||
span: Span,
|
||||
},
|
||||
|
||||
/// Return event, which may become an error if used outside of a function
|
||||
#[error("Return used outside of function")]
|
||||
/// Return event, which may become an error if used outside of a custom command or closure
|
||||
#[error("Return used outside of custom command or closure")]
|
||||
Return {
|
||||
#[label("used outside of function")]
|
||||
#[label("used outside of custom command or closure")]
|
||||
span: Span,
|
||||
value: Box<Value>,
|
||||
},
|
||||
|
@ -1544,8 +1544,8 @@ impl From<io::Error> for ShellError {
|
|||
impl From<Spanned<io::Error>> for ShellError {
|
||||
fn from(error: Spanned<io::Error>) -> Self {
|
||||
let Spanned { item: error, span } = error;
|
||||
if error.kind() == io::ErrorKind::Other {
|
||||
match error.into_inner() {
|
||||
match error.kind() {
|
||||
io::ErrorKind::Other => match error.into_inner() {
|
||||
Some(err) => match err.downcast() {
|
||||
Ok(err) => *err,
|
||||
Err(err) => Self::IOErrorSpanned {
|
||||
|
@ -1557,12 +1557,15 @@ impl From<Spanned<io::Error>> for ShellError {
|
|||
msg: "unknown error".into(),
|
||||
span,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Self::IOErrorSpanned {
|
||||
},
|
||||
io::ErrorKind::TimedOut => Self::NetworkFailure {
|
||||
msg: error.to_string(),
|
||||
span,
|
||||
}
|
||||
},
|
||||
_ => Self::IOErrorSpanned {
|
||||
msg: error.to_string(),
|
||||
span,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ impl Module {
|
|||
vec![]
|
||||
} else {
|
||||
vec![(
|
||||
final_name.clone(),
|
||||
normalize_module_name(&final_name),
|
||||
Value::record(
|
||||
const_rows
|
||||
.into_iter()
|
||||
|
@ -425,3 +425,32 @@ impl Module {
|
|||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// normalize module names for exporting as record constant
|
||||
fn normalize_module_name(bytes: &[u8]) -> Vec<u8> {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|x| match is_identifier_byte(*x) {
|
||||
true => *x,
|
||||
false => b'_',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_identifier_byte(b: u8) -> bool {
|
||||
b != b'.'
|
||||
&& b != b'['
|
||||
&& b != b'('
|
||||
&& b != b'{'
|
||||
&& b != b'+'
|
||||
&& b != b'-'
|
||||
&& b != b'*'
|
||||
&& b != b'^'
|
||||
&& b != b'/'
|
||||
&& b != b'='
|
||||
&& b != b'!'
|
||||
&& b != b'<'
|
||||
&& b != b'>'
|
||||
&& b != b'&'
|
||||
&& b != b'|'
|
||||
}
|
||||
|
|
|
@ -36,5 +36,5 @@ export def "from ndnuon" []: [string -> any] {
|
|||
|
||||
# Convert structured data to NDNUON, i.e. newline-delimited NUON
|
||||
export def "to ndnuon" []: [any -> string] {
|
||||
each { to nuon --raw } | to text
|
||||
each { to nuon --raw | str replace --all "\n" '\n' } | to text
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ def valid-annotations [] {
|
|||
# Returns a table containing the list of function names together with their annotations (comments above the declaration)
|
||||
def get-annotated [
|
||||
file: path
|
||||
] path -> table<function_name: string, annotation: string> {
|
||||
]: path -> table<function_name: string, annotation: string> {
|
||||
let raw_file = (
|
||||
open $file
|
||||
| lines
|
||||
|
@ -59,7 +59,7 @@ def get-annotated [
|
|||
# Annotations that allow multiple functions are of type list<string>
|
||||
# Other annotations are of type string
|
||||
# Result gets merged with the template record so that the output shape remains consistent regardless of the table content
|
||||
def create-test-record [] nothing -> record<before-each: string, after-each: string, before-all: string, after-all: string, test: list<string>, test-skip: list<string>> {
|
||||
def create-test-record []: nothing -> record<before-each: string, after-each: string, before-all: string, after-all: string, test: list<string>, test-skip: list<string>> {
|
||||
let input = $in
|
||||
|
||||
let template_record = {
|
||||
|
@ -187,7 +187,7 @@ export def ($test_function_name) [] {
|
|||
def run-tests-for-module [
|
||||
module: record<file: path name: string before-each: string after-each: string before-all: string after-all: string test: list test-skip: list>
|
||||
threads: int
|
||||
] -> table<file: path, name: string, test: string, result: string> {
|
||||
]: nothing -> table<file: path, name: string, test: string, result: string> {
|
||||
let global_context = if not ($module.before-all|is-empty) {
|
||||
log info $"Running before-all for module ($module.name)"
|
||||
run-test {
|
||||
|
|
|
@ -128,3 +128,17 @@ def to_ndnuon_single_object [] {
|
|||
let expect = "{a: 1}"
|
||||
assert equal $result $expect "could not convert to NDNUON"
|
||||
}
|
||||
|
||||
#[test]
|
||||
def to_ndnuon_multiline_strings [] {
|
||||
let result = "foo\n\\n\nbar" | to ndnuon
|
||||
let expect = '"foo\n\\n\nbar"'
|
||||
assert equal $result $expect "could not convert multiline string to NDNUON"
|
||||
}
|
||||
|
||||
#[test]
|
||||
def from_ndnuon_multiline_strings [] {
|
||||
let result = '"foo\n\\n\nbar"' | from ndnuon
|
||||
let expect = ["foo\n\\n\nbar"]
|
||||
assert equal $result $expect "could not convert multiline string from NDNUON"
|
||||
}
|
||||
|
|
|
@ -128,3 +128,17 @@ def to_ndnuon_single_object [] {
|
|||
let expect = "{a: 1}"
|
||||
assert equal $result $expect "could not convert to NDNUON"
|
||||
}
|
||||
|
||||
#[test]
|
||||
def to_ndnuon_multiline_strings [] {
|
||||
let result = "foo\n\\n\nbar" | formats to ndnuon
|
||||
let expect = '"foo\n\\n\nbar"'
|
||||
assert equal $result $expect "could not convert multiline string to NDNUON"
|
||||
}
|
||||
|
||||
#[test]
|
||||
def from_ndnuon_multiline_strings [] {
|
||||
let result = '"foo\n\\n\nbar"' | formats from ndnuon
|
||||
let expect = ["foo\n\\n\nbar"]
|
||||
assert equal $result $expect "could not convert multiline string from NDNUON"
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use crate::{
|
|||
};
|
||||
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
|
||||
use nu_protocol::{
|
||||
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
|
||||
Value,
|
||||
Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Spanned,
|
||||
SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -26,7 +26,7 @@ impl PluginCommand for WithColumn {
|
|||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.named("name", SyntaxShape::String, "new column name", Some('n'))
|
||||
.named("name", SyntaxShape::String, "New column name. For lazy dataframes and expressions syntax, use a `polars as` expression to name a column.", Some('n'))
|
||||
.rest(
|
||||
"series or expressions",
|
||||
SyntaxShape::Any,
|
||||
|
@ -138,6 +138,15 @@ fn command_eager(
|
|||
let column_span = new_column.span();
|
||||
|
||||
if NuExpression::can_downcast(&new_column) {
|
||||
if let Some(name) = call.get_flag::<Spanned<String>>("name")? {
|
||||
return Err(ShellError::GenericError {
|
||||
error: "Flag 'name' is unsuppored when used with expressions. Please use the `polars as` expression to name a column".into(),
|
||||
msg: "".into(),
|
||||
span: Some(name.span),
|
||||
help: Some("Use a `polars as` expression to name a column".into()),
|
||||
inner: vec![],
|
||||
});
|
||||
}
|
||||
let vals: Vec<Value> = call.rest(0)?;
|
||||
let value = Value::list(vals, call.head);
|
||||
let expressions = NuExpression::extract_exprs(plugin, value)?;
|
||||
|
@ -177,6 +186,16 @@ fn command_lazy(
|
|||
call: &EvaluatedCall,
|
||||
lazy: NuLazyFrame,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if let Some(name) = call.get_flag::<Spanned<String>>("name")? {
|
||||
return Err(ShellError::GenericError {
|
||||
error: "Flag 'name' is unsuppored for lazy dataframes. Please use the `polars as` expression to name a column".into(),
|
||||
msg: "".into(),
|
||||
span: Some(name.span),
|
||||
help: Some("Use a `polars as` expression to name a column".into()),
|
||||
inner: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
let vals: Vec<Value> = call.rest(0)?;
|
||||
let value = Value::list(vals, call.head);
|
||||
let expressions = NuExpression::extract_exprs(plugin, value)?;
|
||||
|
|
|
@ -24,6 +24,7 @@ fn find_id(
|
|||
) -> Option<(Id, usize, Span)> {
|
||||
let file_id = working_set.add_file(file_path.to_string(), file);
|
||||
let offset = working_set.get_span_for_file(file_id).start;
|
||||
let _ = working_set.files.push(file_path.into(), Span::unknown());
|
||||
let block = parse(working_set, Some(file_path), file, false);
|
||||
let flattened = flatten_block(working_set, &block);
|
||||
|
||||
|
@ -88,6 +89,7 @@ pub fn check(engine_state: &mut EngineState, file_path: &str, max_errors: &Value
|
|||
|
||||
if let Ok(contents) = file {
|
||||
let offset = working_set.next_span_start();
|
||||
let _ = working_set.files.push(file_path.into(), Span::unknown());
|
||||
let block = parse(&mut working_set, Some(file_path), &contents, false);
|
||||
|
||||
for (idx, err) in working_set.parse_errors.iter().enumerate() {
|
||||
|
@ -631,6 +633,7 @@ pub fn ast(engine_state: &mut EngineState, file_path: &str) {
|
|||
|
||||
if let Ok(contents) = file {
|
||||
let offset = working_set.next_span_start();
|
||||
let _ = working_set.files.push(file_path.into(), Span::unknown());
|
||||
let parsed_block = parse(&mut working_set, Some(file_path), &contents, false);
|
||||
|
||||
let flat = flatten_block(&working_set, &parsed_block);
|
||||
|
|
|
@ -807,10 +807,10 @@ fn overlay_can_add_renamed_overlay() {
|
|||
#[test]
|
||||
fn overlay_hide_renamed_overlay() {
|
||||
let inp = &[
|
||||
r#"module spam { export def foo [] { "foo" } }"#,
|
||||
r#"module spam { export def foo-command-which-does-not-conflict [] { "foo" } }"#,
|
||||
"overlay use spam as eggs",
|
||||
"overlay hide eggs",
|
||||
"foo",
|
||||
"foo-command-which-does-not-conflict",
|
||||
];
|
||||
|
||||
let actual = nu!(&inp.join("; "));
|
||||
|
@ -1243,9 +1243,9 @@ fn overlay_use_main_def_known_external() {
|
|||
#[test]
|
||||
fn overlay_use_main_not_exported() {
|
||||
let inp = &[
|
||||
r#"module foo { def main [] { "foo" } }"#,
|
||||
"overlay use foo",
|
||||
"foo",
|
||||
r#"module my-super-cool-and-unique-module-name { def main [] { "hi" } }"#,
|
||||
"overlay use my-super-cool-and-unique-module-name",
|
||||
"my-super-cool-and-unique-module-name",
|
||||
];
|
||||
|
||||
let actual = nu!(&inp.join("; "));
|
||||
|
@ -1257,11 +1257,11 @@ fn overlay_use_main_not_exported() {
|
|||
fn alias_overlay_hide() {
|
||||
let inp = &[
|
||||
"overlay new spam",
|
||||
"def foo [] { 'foo' }",
|
||||
"def my-epic-command-name [] { 'foo' }",
|
||||
"overlay new eggs",
|
||||
"alias oh = overlay hide",
|
||||
"oh spam",
|
||||
"foo",
|
||||
"my-epic-command-name",
|
||||
];
|
||||
|
||||
let actual = nu!(&inp.join("; "));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::repl::tests::{fail_test, run_test, TestResult};
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn module_def_imports_1() -> TestResult {
|
||||
|
@ -145,6 +146,28 @@ fn export_module_which_defined_const() -> TestResult {
|
|||
)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("spam-mod")]
|
||||
#[case("spam/mod")]
|
||||
#[case("spam=mod")]
|
||||
fn export_module_with_normalized_var_name(#[case] name: &str) -> TestResult {
|
||||
let def = format!(
|
||||
"module {name} {{ export const b = 3; export module {name}2 {{ export const c = 4 }} }}"
|
||||
);
|
||||
run_test(&format!("{def}; use {name}; $spam_mod.b"), "3")?;
|
||||
run_test(&format!("{def}; use {name} *; $spam_mod2.c"), "4")
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("spam-mod")]
|
||||
#[case("spam/mod")]
|
||||
fn use_module_with_invalid_var_name(#[case] name: &str) -> TestResult {
|
||||
fail_test(
|
||||
&format!("module {name} {{ export const b = 3 }}; use {name}; ${name}"),
|
||||
"expected valid variable name. Did you mean '$spam_mod'",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_export_private_const() -> TestResult {
|
||||
fail_test(
|
||||
|
|
Loading…
Reference in a new issue