Merge branch 'nushell:main' into continue-PWD-per-drive-quote

This commit is contained in:
PegasusPlusUS 2024-12-08 06:23:06 -08:00 committed by GitHub
commit 9078c182e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 457 additions and 89 deletions

4
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -106,6 +106,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
bind_command! {
Path,
PathBasename,
PathSelf,
PathDirname,
PathExists,
PathExpand,

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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