Merge branch 'main' into main

This commit is contained in:
pwygab 2024-05-16 09:18:02 +08:00 committed by GitHub
commit ba8c6a56cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 568 additions and 335 deletions

View file

@ -26,7 +26,7 @@ Make sure you've run and fixed any issues with these commands:
- `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library
> **Note**
> from `nushell` you can also use the `toolkit` as follows

View file

@ -55,7 +55,6 @@ It is good practice to cover your changes with a test. Also, try to think about
Tests can be found in different places:
* `/tests`
* `src/tests`
* command examples
* crate-specific tests

17
Cargo.lock generated
View file

@ -2034,9 +2034,9 @@ dependencies = [
[[package]]
name = "interprocess"
version = "2.0.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c7fb8583fab9503654385e2bafda123376445a77027a1b106dd7e44cf51122f"
checksum = "7b4d0250d41da118226e55b3d50ca3f0d9e0a0f6829b92f543ac0054aeea1572"
dependencies = [
"libc",
"recvmsg",
@ -3259,6 +3259,7 @@ dependencies = [
"nu-test-support",
"nu-utils",
"num-format",
"os_pipe",
"pretty_assertions",
"rmp-serde",
"rstest",
@ -5056,9 +5057,9 @@ dependencies = [
[[package]]
name = "rust-embed"
version = "8.3.0"
version = "8.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745"
checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@ -5067,9 +5068,9 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
version = "8.3.0"
version = "8.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8"
checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4"
dependencies = [
"proc-macro2",
"quote",
@ -5080,9 +5081,9 @@ dependencies = [
[[package]]
name = "rust-embed-utils"
version = "8.3.0"
version = "8.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581"
checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32"
dependencies = [
"sha2",
"walkdir",

View file

@ -94,7 +94,7 @@ heck = "0.5.0"
human-date-parser = "0.1.1"
indexmap = "2.2"
indicatif = "0.17"
interprocess = "2.0.1"
interprocess = "2.1.0"
is_executable = "1.0"
itertools = "0.12"
libc = "0.2"
@ -119,7 +119,7 @@ num-traits = "0.2"
omnipath = "0.1"
once_cell = "1.18"
open = "5.1"
os_pipe = "1.1"
os_pipe = { version = "1.1", features = ["io_safety"] }
pathdiff = "0.2"
percent-encoding = "2"
pretty_assertions = "1.4"
@ -140,7 +140,7 @@ ropey = "1.6.1"
roxmltree = "0.19"
rstest = { version = "0.18", default-features = false }
rusqlite = "0.31"
rust-embed = "8.3.0"
rust-embed = "8.4.0"
same-file = "1.0"
serde = { version = "1.0", default-features = false }
serde_json = "1.0"

View file

@ -542,7 +542,9 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
let shell_integration_osc633 = config.shell_integration_osc633;
let shell_integration_reset_application_mode = config.shell_integration_reset_application_mode;
let mut stack = Stack::unwrap_unique(stack_arc);
// TODO: we may clone the stack, this can lead to major performance issues
// so we should avoid it or making stack cheaper to clone.
let mut stack = Arc::unwrap_or_clone(stack_arc);
perf(
"line_editor setup",

View file

@ -10,7 +10,7 @@ impl Command for EachWhile {
}
fn usage(&self) -> &str {
"Run a block on each row of the input list until a null is found, then create a new list with the results."
"Run a closure on each row of the input list until a null is found, then create a new list with the results."
}
fn search_terms(&self) -> Vec<&str> {

View file

@ -5,7 +5,7 @@ use commands::add_commands_decls;
pub use values::{
convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_connection_in_memory,
open_connection_in_memory_custom, SQLiteDatabase, MEMORY_DB,
open_connection_in_memory_custom, values_to_sql, SQLiteDatabase, MEMORY_DB,
};
use nu_protocol::engine::StateWorkingSet;

View file

@ -3,5 +3,5 @@ pub mod sqlite;
pub use sqlite::{
convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_connection_in_memory,
open_connection_in_memory_custom, SQLiteDatabase, MEMORY_DB,
open_connection_in_memory_custom, values_to_sql, SQLiteDatabase, MEMORY_DB,
};

View file

@ -90,10 +90,7 @@ fn with_env(
return Err(ShellError::CantConvert {
to_type: "record".into(),
from_type: x.get_type().to_string(),
span: call
.positional_nth(1)
.expect("already checked through .req")
.span,
span: x.span(),
help: None,
});
}
@ -124,10 +121,7 @@ fn with_env(
return Err(ShellError::CantConvert {
to_type: "record".into(),
from_type: x.get_type().to_string(),
span: call
.positional_nth(1)
.expect("already checked through .req")
.span,
span: x.span(),
help: None,
});
}

View file

@ -117,7 +117,7 @@ impl Command for Touch {
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
for (index, glob) in files.into_iter().enumerate() {
for glob in files {
let path = expand_path_with(glob.item.as_ref(), &cwd, glob.item.is_expand());
// If --no-create is passed and the file/dir does not exist there's nothing to do
@ -135,10 +135,7 @@ impl Command for Touch {
{
return Err(ShellError::CreateNotPossible {
msg: format!("Failed to create file: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
span: glob.span,
});
};
}
@ -148,10 +145,7 @@ impl Command for Touch {
{
return Err(ShellError::ChangeModifiedTimeNotPossible {
msg: format!("Failed to change the modified time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
span: glob.span,
});
};
}
@ -161,10 +155,7 @@ impl Command for Touch {
{
return Err(ShellError::ChangeAccessTimeNotPossible {
msg: format!("Failed to change the access time: {err}"),
span: call
.positional_nth(index)
.expect("already checked positional")
.span,
span: glob.span,
});
};
}

View file

@ -31,6 +31,10 @@ impl Command for Compact {
"Creates a table with non-empty rows."
}
fn search_terms(&self) -> Vec<&str> {
vec!["empty", "remove"]
}
fn run(
&self,
engine_state: &EngineState,

View file

@ -115,7 +115,7 @@ If multiple cell paths are given, this will produce a list of values."#
},
Example {
description:
"Extract the name of the 3rd record in a list (same as `ls | $in.name`)",
"Extract the name of the 3rd record in a list (same as `ls | $in.name.2`)",
example: "ls | get name.2",
result: None,
},

View file

@ -784,10 +784,7 @@ fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result<St
None => {
return Err(ShellError::TypeMismatch {
err_message: String::from("Unknown ansi code"),
span: call
.positional_nth(0)
.expect("Unexpected missing argument")
.span,
span: code.span(),
})
}
}

View file

@ -1,5 +1,6 @@
use crate::database::{SQLiteDatabase, MEMORY_DB};
use crate::database::{values_to_sql, SQLiteDatabase, MEMORY_DB};
use nu_engine::command_prelude::*;
use rusqlite::params_from_iter;
#[derive(Clone)]
pub struct StorInsert;
@ -57,86 +58,81 @@ impl Command for StorInsert {
// let config = engine_state.get_config();
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
if table_name.is_none() {
return Err(ShellError::MissingParameter {
param_name: "requires at table name".into(),
span,
});
}
let new_table_name = table_name.unwrap_or("table".into());
if let Ok(conn) = db.open_connection() {
match columns {
Some(record) => {
let mut create_stmt = format!("INSERT INTO {} ( ", new_table_name);
let cols = record.columns();
cols.for_each(|col| {
create_stmt.push_str(&format!("{}, ", col));
});
if create_stmt.ends_with(", ") {
create_stmt.pop();
create_stmt.pop();
}
process(table_name, span, &db, columns)?;
create_stmt.push_str(") VALUES ( ");
let vals = record.values();
vals.for_each(|val| match val {
Value::Int { val, .. } => {
create_stmt.push_str(&format!("{}, ", val));
}
Value::Float { val, .. } => {
create_stmt.push_str(&format!("{}, ", val));
}
Value::String { val, .. } => {
create_stmt.push_str(&format!("'{}', ", val));
}
Value::Date { val, .. } => {
create_stmt.push_str(&format!("'{}', ", val));
}
Value::Bool { val, .. } => {
create_stmt.push_str(&format!("{}, ", val));
}
_ => {
// return Err(ShellError::UnsupportedInput {
// msg: format!("{} is not a valid datepart, expected one of year, month, day, hour, minute, second, millisecond, microsecond, nanosecond", part.item),
// input: "value originates from here".to_string(),
// msg_span: span,
// input_span: val.span(),
// });
}
});
if create_stmt.ends_with(", ") {
create_stmt.pop();
create_stmt.pop();
}
create_stmt.push(')');
// dbg!(&create_stmt);
conn.execute(&create_stmt, [])
.map_err(|err| ShellError::GenericError {
error: "Failed to open SQLite connection in memory from insert".into(),
msg: err.to_string(),
span: Some(Span::test_data()),
help: None,
inner: vec![],
})?;
}
None => {
return Err(ShellError::MissingParameter {
param_name: "requires at least one column".into(),
span: call.head,
});
}
};
}
// dbg!(db.clone());
Ok(Value::custom(db, span).into_pipeline_data())
}
}
fn process(
table_name: Option<String>,
span: Span,
db: &SQLiteDatabase,
columns: Option<Record>,
) -> Result<(), ShellError> {
if table_name.is_none() {
return Err(ShellError::MissingParameter {
param_name: "requires at table name".into(),
span,
});
}
let new_table_name = table_name.unwrap_or("table".into());
if let Ok(conn) = db.open_connection() {
match columns {
Some(record) => {
let mut create_stmt = format!("INSERT INTO {} ( ", new_table_name);
let cols = record.columns();
cols.for_each(|col| {
create_stmt.push_str(&format!("{}, ", col));
});
if create_stmt.ends_with(", ") {
create_stmt.pop();
create_stmt.pop();
}
// Values are set as placeholders.
create_stmt.push_str(") VALUES ( ");
for (index, _) in record.columns().enumerate() {
create_stmt.push_str(&format!("?{}, ", index + 1));
}
if create_stmt.ends_with(", ") {
create_stmt.pop();
create_stmt.pop();
}
create_stmt.push(')');
// dbg!(&create_stmt);
// Get the params from the passed values
let params = values_to_sql(record.values().cloned())?;
conn.execute(&create_stmt, params_from_iter(params))
.map_err(|err| ShellError::GenericError {
error: "Failed to open SQLite connection in memory from insert".into(),
msg: err.to_string(),
span: Some(Span::test_data()),
help: None,
inner: vec![],
})?;
}
None => {
return Err(ShellError::MissingParameter {
param_name: "requires at least one column".into(),
span,
});
}
};
}
// dbg!(db.clone());
Ok(())
}
#[cfg(test)]
mod test {
use chrono::DateTime;
use super::*;
#[test]
@ -145,4 +141,160 @@ mod test {
test_examples(StorInsert {})
}
#[test]
fn test_process_with_simple_parameters() {
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
let create_stmt = "CREATE TABLE test_process_with_simple_parameters (
int_column INTEGER,
real_column REAL,
str_column VARCHAR(255),
bool_column BOOLEAN,
date_column DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
)";
let conn = db
.open_connection()
.expect("Test was unable to open connection.");
conn.execute(create_stmt, [])
.expect("Failed to create table as part of test.");
let table_name = Some("test_process_with_simple_parameters".to_string());
let span = Span::unknown();
let mut columns = Record::new();
columns.insert("int_column".to_string(), Value::test_int(42));
columns.insert("real_column".to_string(), Value::test_float(3.1));
columns.insert(
"str_column".to_string(),
Value::test_string("SimpleString".to_string()),
);
columns.insert("bool_column".to_string(), Value::test_bool(true));
columns.insert(
"date_column".to_string(),
Value::test_date(
DateTime::parse_from_str("2021-12-30 00:00:00 +0000", "%Y-%m-%d %H:%M:%S %z")
.expect("Date string should parse."),
),
);
let result = process(table_name, span, &db, Some(columns));
assert!(result.is_ok());
}
#[test]
fn test_process_string_with_space() {
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
let create_stmt = "CREATE TABLE test_process_string_with_space (
str_column VARCHAR(255)
)";
let conn = db
.open_connection()
.expect("Test was unable to open connection.");
conn.execute(create_stmt, [])
.expect("Failed to create table as part of test.");
let table_name = Some("test_process_string_with_space".to_string());
let span = Span::unknown();
let mut columns = Record::new();
columns.insert(
"str_column".to_string(),
Value::test_string("String With Spaces".to_string()),
);
let result = process(table_name, span, &db, Some(columns));
assert!(result.is_ok());
}
#[test]
fn test_no_errors_when_string_too_long() {
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
let create_stmt = "CREATE TABLE test_errors_when_string_too_long (
str_column VARCHAR(8)
)";
let conn = db
.open_connection()
.expect("Test was unable to open connection.");
conn.execute(create_stmt, [])
.expect("Failed to create table as part of test.");
let table_name = Some("test_errors_when_string_too_long".to_string());
let span = Span::unknown();
let mut columns = Record::new();
columns.insert(
"str_column".to_string(),
Value::test_string("ThisIsALongString".to_string()),
);
let result = process(table_name, span, &db, Some(columns));
// SQLite uses dynamic typing, making any length acceptable for a varchar column
assert!(result.is_ok());
}
#[test]
fn test_no_errors_when_param_is_wrong_type() {
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
let create_stmt = "CREATE TABLE test_errors_when_param_is_wrong_type (
int_column INT
)";
let conn = db
.open_connection()
.expect("Test was unable to open connection.");
conn.execute(create_stmt, [])
.expect("Failed to create table as part of test.");
let table_name = Some("test_errors_when_param_is_wrong_type".to_string());
let span = Span::unknown();
let mut columns = Record::new();
columns.insert(
"int_column".to_string(),
Value::test_string("ThisIsTheWrongType".to_string()),
);
let result = process(table_name, span, &db, Some(columns));
// SQLite uses dynamic typing, making any type acceptable for a column
assert!(result.is_ok());
}
#[test]
fn test_errors_when_column_doesnt_exist() {
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
let create_stmt = "CREATE TABLE test_errors_when_column_doesnt_exist (
int_column INT
)";
let conn = db
.open_connection()
.expect("Test was unable to open connection.");
conn.execute(create_stmt, [])
.expect("Failed to create table as part of test.");
let table_name = Some("test_errors_when_column_doesnt_exist".to_string());
let span = Span::unknown();
let mut columns = Record::new();
columns.insert(
"not_a_column".to_string(),
Value::test_string("ThisIsALongString".to_string()),
);
let result = process(table_name, span, &db, Some(columns));
assert!(result.is_err());
}
#[test]
fn test_errors_when_table_doesnt_exist() {
let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None));
let table_name = Some("test_errors_when_table_doesnt_exist".to_string());
let span = Span::unknown();
let mut columns = Record::new();
columns.insert(
"str_column".to_string(),
Value::test_string("ThisIsALongString".to_string()),
);
let result = process(table_name, span, &db, Some(columns));
assert!(result.is_err());
}
}

View file

@ -235,18 +235,18 @@ impl Command for Char {
// handle -i flag
if integer {
let int_args: Vec<i64> = call.rest_const(working_set, 0)?;
handle_integer_flag(int_args, call, call_span)
let int_args = call.rest_const(working_set, 0)?;
handle_integer_flag(int_args, call_span)
}
// handle -u flag
else if unicode {
let string_args: Vec<String> = call.rest_const(working_set, 0)?;
handle_unicode_flag(string_args, call, call_span)
let string_args = call.rest_const(working_set, 0)?;
handle_unicode_flag(string_args, call_span)
}
// handle the rest
else {
let string_args: Vec<String> = call.rest_const(working_set, 0)?;
handle_the_rest(string_args, call, call_span)
let string_args = call.rest_const(working_set, 0)?;
handle_the_rest(string_args, call_span)
}
}
@ -270,18 +270,18 @@ impl Command for Char {
// handle -i flag
if integer {
let int_args: Vec<i64> = call.rest(engine_state, stack, 0)?;
handle_integer_flag(int_args, call, call_span)
let int_args = call.rest(engine_state, stack, 0)?;
handle_integer_flag(int_args, call_span)
}
// handle -u flag
else if unicode {
let string_args: Vec<String> = call.rest(engine_state, stack, 0)?;
handle_unicode_flag(string_args, call, call_span)
let string_args = call.rest(engine_state, stack, 0)?;
handle_unicode_flag(string_args, call_span)
}
// handle the rest
else {
let string_args: Vec<String> = call.rest(engine_state, stack, 0)?;
handle_the_rest(string_args, call, call_span)
let string_args = call.rest(engine_state, stack, 0)?;
handle_the_rest(string_args, call_span)
}
}
}
@ -309,8 +309,7 @@ fn generate_character_list(ctrlc: Option<Arc<AtomicBool>>, call_span: Span) -> P
}
fn handle_integer_flag(
int_args: Vec<i64>,
call: &Call,
int_args: Vec<Spanned<i64>>,
call_span: Span,
) -> Result<PipelineData, ShellError> {
if int_args.is_empty() {
@ -319,20 +318,17 @@ fn handle_integer_flag(
span: call_span,
});
}
let mut multi_byte = String::new();
for (i, &arg) in int_args.iter().enumerate() {
let span = call
.positional_nth(i)
.expect("Unexpected missing argument")
.span;
multi_byte.push(integer_to_unicode_char(arg, span)?)
}
Ok(Value::string(multi_byte, call_span).into_pipeline_data())
let str = int_args
.into_iter()
.map(integer_to_unicode_char)
.collect::<Result<String, _>>()?;
Ok(Value::string(str, call_span).into_pipeline_data())
}
fn handle_unicode_flag(
string_args: Vec<String>,
call: &Call,
string_args: Vec<Spanned<String>>,
call_span: Span,
) -> Result<PipelineData, ShellError> {
if string_args.is_empty() {
@ -341,57 +337,53 @@ fn handle_unicode_flag(
span: call_span,
});
}
let mut multi_byte = String::new();
for (i, arg) in string_args.iter().enumerate() {
let span = call
.positional_nth(i)
.expect("Unexpected missing argument")
.span;
multi_byte.push(string_to_unicode_char(arg, span)?)
}
Ok(Value::string(multi_byte, call_span).into_pipeline_data())
let str = string_args
.into_iter()
.map(string_to_unicode_char)
.collect::<Result<String, _>>()?;
Ok(Value::string(str, call_span).into_pipeline_data())
}
fn handle_the_rest(
string_args: Vec<String>,
call: &Call,
string_args: Vec<Spanned<String>>,
call_span: Span,
) -> Result<PipelineData, ShellError> {
if string_args.is_empty() {
let Some(s) = string_args.first() else {
return Err(ShellError::MissingParameter {
param_name: "missing name of the character".into(),
span: call_span,
});
}
let special_character = str_to_character(&string_args[0]);
};
let special_character = str_to_character(&s.item);
if let Some(output) = special_character {
Ok(Value::string(output, call_span).into_pipeline_data())
} else {
Err(ShellError::TypeMismatch {
err_message: "error finding named character".into(),
span: call
.positional_nth(0)
.expect("Unexpected missing argument")
.span,
span: s.span,
})
}
}
fn integer_to_unicode_char(value: i64, t: Span) -> Result<char, ShellError> {
let decoded_char = value.try_into().ok().and_then(std::char::from_u32);
fn integer_to_unicode_char(value: Spanned<i64>) -> Result<char, ShellError> {
let decoded_char = value.item.try_into().ok().and_then(std::char::from_u32);
if let Some(ch) = decoded_char {
Ok(ch)
} else {
Err(ShellError::TypeMismatch {
err_message: "not a valid Unicode codepoint".into(),
span: t,
span: value.span,
})
}
}
fn string_to_unicode_char(s: &str, t: Span) -> Result<char, ShellError> {
let decoded_char = u32::from_str_radix(s, 16)
fn string_to_unicode_char(s: Spanned<String>) -> Result<char, ShellError> {
let decoded_char = u32::from_str_radix(&s.item, 16)
.ok()
.and_then(std::char::from_u32);
@ -400,7 +392,7 @@ fn string_to_unicode_char(s: &str, t: Span) -> Result<char, ShellError> {
} else {
Err(ShellError::TypeMismatch {
err_message: "error decoding Unicode character".into(),
span: t,
span: s.span,
})
}
}

View file

@ -16,9 +16,8 @@ pub use sys_::Sys;
pub use temp::SysTemp;
pub use users::SysUsers;
use chrono::{DateTime, Local};
use chrono::{DateTime, FixedOffset, Local};
use nu_protocol::{record, Record, Span, Value};
use std::time::{Duration, UNIX_EPOCH};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
};
@ -171,22 +170,31 @@ pub fn host(span: Span) -> Record {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
record.push(
"uptime",
Value::duration(1000000000 * System::uptime() as i64, span),
);
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
// Creates a new SystemTime from the specified number of whole seconds
let d = UNIX_EPOCH + Duration::from_secs(System::boot_time());
// Create DateTime from SystemTime
let datetime = DateTime::<Local>::from(d);
// Convert to local time and then rfc3339
let timestamp_str = datetime.with_timezone(datetime.offset()).to_rfc3339();
record.push("boot_time", Value::string(timestamp_str, span));
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
record
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}
pub fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()

View file

@ -20,7 +20,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use nu_color_config::{get_color_map, StyleComputer};
use nu_protocol::{
engine::{EngineState, Stack},
Record, Value,
Record, Span, Value,
};
use ratatui::{layout::Rect, widgets::Block};
use std::{borrow::Cow, collections::HashMap};
@ -180,7 +180,11 @@ impl<'a> RecordView<'a> {
Orientation::Left => (column, row),
};
layer.records[row][column].clone()
if layer.records.len() > row && layer.records[row].len() > column {
layer.records[row][column].clone()
} else {
Value::nothing(Span::unknown())
}
}
fn create_tablew(&'a self, cfg: ViewConfig<'a>) -> TableW<'a> {

View file

@ -132,7 +132,7 @@ pub fn lex_item(
},
Some(ParseError::UnexpectedEof(
(start as char).to_string(),
Span::new(span.end, span.end),
Span::new(span.end - 1, span.end),
)),
);
}
@ -262,26 +262,10 @@ pub fn lex_item(
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
// If there is still unclosed opening delimiters, remember they were missing
if let Some(block) = block_level.last() {
let delim = block.closing();
let cause =
ParseError::UnexpectedEof((delim as char).to_string(), Span::new(span.end, span.end));
return (
Token {
contents: TokenContents::Item,
span,
},
Some(cause),
);
}
if let Some(delim) = quote_start {
// The non-lite parse trims quotes on both sides, so we add the expected quote so that
// anyone wanting to consume this partial parse (e.g., completions) will be able to get
// correct information from the non-lite parse.
return (
Token {
contents: TokenContents::Item,
@ -289,11 +273,28 @@ pub fn lex_item(
},
Some(ParseError::UnexpectedEof(
(delim as char).to_string(),
Span::new(span.start, span.end),
Span::new(span.end - 1, span.end),
)),
);
}
// If there is still unclosed opening delimiters, remember they were missing
if let Some(block) = block_level.last() {
let delim = block.closing();
let cause = ParseError::UnexpectedEof(
(delim as char).to_string(),
Span::new(span.end - 1, span.end),
);
return (
Token {
contents: TokenContents::Item,
span,
},
Some(cause),
);
}
// If we didn't accumulate any characters, it's an unexpected error.
if *curr_offset - token_start == 0 {
return (
@ -411,9 +412,11 @@ fn lex_raw_string(
*curr_offset += 1
}
if !matches {
let mut expected = '\''.to_string();
expected.push_str(&"#".repeat(prefix_sharp_cnt));
return Err(ParseError::UnexpectedEof(
"#".to_string(),
Span::new(span_offset + *curr_offset, span_offset + *curr_offset),
expected,
Span::new(span_offset + *curr_offset - 1, span_offset + *curr_offset),
));
}
Ok(())

View file

@ -47,6 +47,7 @@ nu-test-support = { path = "../nu-test-support", version = "0.93.1" }
pretty_assertions = { workspace = true }
rstest = { workspace = true }
tempfile = { workspace = true }
os_pipe = { workspace = true }
[package.metadata.docs.rs]
all-features = true

View file

@ -154,30 +154,10 @@ impl Call {
})
}
pub fn positional_iter_mut(&mut self) -> impl Iterator<Item = &mut Expression> {
self.arguments
.iter_mut()
.take_while(|arg| match arg {
Argument::Spread(_) => false, // Don't include positional arguments given to rest parameter
_ => true,
})
.filter_map(|arg| match arg {
Argument::Named(_) => None,
Argument::Positional(positional) => Some(positional),
Argument::Unknown(unknown) => Some(unknown),
Argument::Spread(_) => None,
})
}
pub fn positional_nth(&self, i: usize) -> Option<&Expression> {
self.positional_iter().nth(i)
}
// TODO this method is never used. Delete?
pub fn positional_nth_mut(&mut self, i: usize) -> Option<&mut Expression> {
self.positional_iter_mut().nth(i)
}
pub fn positional_len(&self) -> usize {
self.positional_iter().count()
}

View file

@ -7,6 +7,7 @@ use crate::{
};
use std::{
collections::{HashMap, HashSet},
fs::File,
sync::Arc,
};
@ -74,26 +75,10 @@ impl Stack {
}
}
/// Unwrap a uniquely-owned stack.
///
/// In debug mode, this panics if there are multiple references.
/// In production this will instead clone the underlying stack.
pub fn unwrap_unique(stack_arc: Arc<Stack>) -> Stack {
// If you hit an error here, it's likely that you created an extra
// Arc pointing to the stack somewhere. Make sure that it gets dropped before
// getting here!
Arc::try_unwrap(stack_arc).unwrap_or_else(|arc| {
// in release mode, we clone the stack, but this can lead to
// major performance issues, so we should avoid it
debug_assert!(false, "More than one stack reference remaining!");
(*arc).clone()
})
}
/// Create a new child stack from a parent.
///
/// Changes from this child can be merged back into the parent with
/// Stack::with_changes_from_child
/// [`Stack::with_changes_from_child`]
pub fn with_parent(parent: Arc<Stack>) -> Stack {
Stack {
// here we are still cloning environment variable-related information
@ -108,19 +93,17 @@ impl Stack {
}
}
/// Take an Arc of a parent (assumed to be unique), and a child, and apply
/// all the changes from a child back to the parent.
/// Take an [`Arc`] parent, and a child, and apply all the changes from a child back to the parent.
///
/// Here it is assumed that child was created with a call to Stack::with_parent
/// with parent
/// Here it is assumed that `child` was created by a call to [`Stack::with_parent`] with `parent`.
///
/// For this to be performant and not clone `parent`, `child` should be the only other
/// referencer of `parent`.
pub fn with_changes_from_child(parent: Arc<Stack>, child: Stack) -> Stack {
// we're going to drop the link to the parent stack on our new stack
// so that we can unwrap the Arc as a unique reference
//
// This makes the new_stack be in a bit of a weird state, so we shouldn't call
// any structs
drop(child.parent_stack);
let mut unique_stack = Stack::unwrap_unique(parent);
let mut unique_stack = Arc::unwrap_or_clone(parent);
unique_stack
.vars
@ -593,6 +576,57 @@ impl Stack {
self
}
/// Replaces the default stdout of the stack with a given file.
///
/// This method configures the default stdout to redirect to a specified file.
/// It is primarily useful for applications using `nu` as a language, where the stdout of
/// external commands that are not explicitly piped can be redirected to a file.
///
/// # Using Pipes
///
/// For use in third-party applications pipes might be very useful as they allow using the
/// stdout of external commands for different uses.
/// For example the [`os_pipe`](https://docs.rs/os_pipe) crate provides a elegant way to to
/// access the stdout.
///
/// ```
/// # use std::{fs::File, io::{self, Read}, thread, error};
/// # use nu_protocol::engine::Stack;
/// #
/// let (mut reader, writer) = os_pipe::pipe().unwrap();
/// // Use a thread to avoid blocking the execution of the called command.
/// let reader = thread::spawn(move || {
/// let mut buf: Vec<u8> = Vec::new();
/// reader.read_to_end(&mut buf)?;
/// Ok::<_, io::Error>(buf)
/// });
///
/// #[cfg(windows)]
/// let file = std::os::windows::io::OwnedHandle::from(writer).into();
/// #[cfg(unix)]
/// let file = std::os::unix::io::OwnedFd::from(writer).into();
///
/// let stack = Stack::new().stdout_file(file);
///
/// // Execute some nu code.
///
/// drop(stack); // drop the stack so that the writer will be dropped too
/// let buf = reader.join().unwrap().unwrap();
/// // Do with your buffer whatever you want.
/// ```
pub fn stdout_file(mut self, file: File) -> Self {
self.out_dest.stdout = OutDest::File(Arc::new(file));
self
}
/// Replaces the default stderr of the stack with a given file.
///
/// For more info, see [`stdout_file`](Self::stdout_file).
pub fn stderr_file(mut self, file: File) -> Self {
self.out_dest.stderr = OutDest::File(Arc::new(file));
self
}
/// Set the PWD environment variable to `path`.
///
/// This method accepts `path` with trailing slashes, but they're removed

View file

@ -287,7 +287,7 @@ export def run-tests [
--list, # list the selected tests without running them.
--threads: int@"nu-complete threads", # Amount of threads to use for parallel execution. Default: All threads are utilized
] {
let available_threads = (sys | get cpu | length)
let available_threads = (sys cpu | length)
# Can't use pattern matching here due to https://github.com/nushell/nushell/issues/9198
let threads = (if $threads == null {

View file

@ -38,7 +38,7 @@ impl PluginCommand for Summary {
)
.named(
"quantiles",
SyntaxShape::Table(vec![]),
SyntaxShape::List(Box::new(SyntaxShape::Float)),
"provide optional quantiles",
Some('q'),
)

View file

@ -27,7 +27,7 @@ impl NuDataFrame {
let rhs_span = right.span();
match right {
Value::Custom { .. } => {
let rhs = NuDataFrame::try_from_value(plugin, right)?;
let rhs = NuDataFrame::try_from_value_coerce(plugin, right, rhs_span)?;
match (self.is_series(), rhs.is_series()) {
(true, true) => {

View file

@ -7,7 +7,7 @@ use uuid::Uuid;
use crate::{
values::{CustomValueSupport, NuDataFrame, PolarsPluginCustomValue},
PolarsPlugin,
Cacheable, PolarsPlugin,
};
use super::NuLazyFrame;
@ -76,11 +76,49 @@ impl PolarsPluginCustomValue for NuLazyFrameCustomValue {
_engine: &EngineInterface,
other_value: Value,
) -> Result<Option<Ordering>, ShellError> {
// to compare, we need to convert to NuDataframe
let df = NuLazyFrame::try_from_custom_value(plugin, self)?;
let df = df.collect(other_value.span())?;
let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?;
let other = NuDataFrame::try_from_value_coerce(plugin, &other_value, other_value.span())?;
let res = df.is_equal(&other);
let res = eager.is_equal(&other);
Ok(res)
}
fn custom_value_operation(
&self,
plugin: &PolarsPlugin,
engine: &EngineInterface,
lhs_span: Span,
operator: nu_protocol::Spanned<nu_protocol::ast::Operator>,
right: Value,
) -> Result<Value, ShellError> {
let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?;
Ok(eager
.compute_with_value(plugin, lhs_span, operator.item, operator.span, &right)?
.cache(plugin, engine, lhs_span)?
.into_value(lhs_span))
}
fn custom_value_follow_path_int(
&self,
plugin: &PolarsPlugin,
_engine: &EngineInterface,
_self_span: Span,
index: nu_protocol::Spanned<usize>,
) -> Result<Value, ShellError> {
let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?;
eager.get_value(index.item, index.span)
}
fn custom_value_follow_path_string(
&self,
plugin: &PolarsPlugin,
engine: &EngineInterface,
self_span: Span,
column_name: nu_protocol::Spanned<String>,
) -> Result<Value, ShellError> {
let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?;
let column = eager.column(&column_name.item, self_span)?;
Ok(column
.cache(plugin, engine, self_span)?
.into_value(self_span))
}
}

View file

@ -1,4 +1,4 @@
use log::{info, trace};
use log::warn;
#[cfg(feature = "plugin")]
use nu_cli::read_plugin_file;
use nu_cli::{eval_config_contents, eval_source};
@ -27,7 +27,10 @@ pub(crate) fn read_config_file(
config_file: Option<Spanned<String>>,
is_env_config: bool,
) {
trace!("read_config_file {:?}", &config_file);
warn!(
"read_config_file() config_file_specified: {:?}, is_env_config: {is_env_config}",
&config_file
);
// Load config startup file
if let Some(file) = config_file {
let working_set = StateWorkingSet::new(engine_state);
@ -122,21 +125,27 @@ pub(crate) fn read_config_file(
}
pub(crate) fn read_loginshell_file(engine_state: &mut EngineState, stack: &mut Stack) {
warn!(
"read_loginshell_file() {}:{}:{}",
file!(),
line!(),
column!()
);
// read and execute loginshell file if exists
if let Some(mut config_path) = nu_path::config_dir() {
config_path.push(NUSHELL_FOLDER);
config_path.push(LOGINSHELL_FILE);
warn!("loginshell_file: {}", config_path.display());
if config_path.exists() {
eval_config_contents(config_path, engine_state, stack);
}
}
info!("read_loginshell_file {}:{}:{}", file!(), line!(), column!());
}
pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut Stack) {
trace!("read_default_env_file");
let config_file = get_default_env();
eval_source(
engine_state,
@ -147,7 +156,13 @@ pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut
false,
);
info!("read_config_file {}:{}:{}", file!(), line!(), column!());
warn!(
"read_default_env_file() env_file_contents: {config_file} {}:{}:{}",
file!(),
line!(),
column!()
);
// Merge the environment in case env vars changed in the config
match engine_state.cwd(Some(stack)) {
Ok(cwd) => {
@ -167,10 +182,9 @@ fn eval_default_config(
config_file: &str,
is_env_config: bool,
) {
trace!(
"eval_default_config: config_file: {:?}, is_env_config: {}",
&config_file,
is_env_config
warn!(
"eval_default_config() config_file_specified: {:?}, is_env_config: {}",
&config_file, is_env_config
);
println!("Continuing without config file");
// Just use the contents of "default_config.nu" or "default_env.nu"
@ -208,11 +222,9 @@ pub(crate) fn setup_config(
env_file: Option<Spanned<String>>,
is_login_shell: bool,
) {
trace!(
"setup_config: config: {:?}, env: {:?}, login: {}",
&config_file,
&env_file,
is_login_shell
warn!(
"setup_config() config_file_specified: {:?}, env_file_specified: {:?}, login: {}",
&config_file, &env_file, is_login_shell
);
let result = catch_unwind(AssertUnwindSafe(|| {
#[cfg(feature = "plugin")]
@ -240,12 +252,9 @@ pub(crate) fn set_config_path(
key: &str,
config_file: Option<&Spanned<String>>,
) {
trace!(
"set_config_path: cwd: {:?}, default_config: {}, key: {}, config_file: {:?}",
&cwd,
&default_config_name,
&key,
&config_file
warn!(
"set_config_path() cwd: {:?}, default_config: {}, key: {}, config_file_specified: {:?}",
&cwd, &default_config_name, &key, &config_file
);
let config_path = match config_file {
Some(s) => canonicalize_with(&s.item, cwd).ok(),

View file

@ -7,8 +7,6 @@ mod signals;
#[cfg(unix)]
mod terminal;
mod test_bins;
#[cfg(test)]
mod tests;
#[cfg(feature = "mimalloc")]
#[global_allocator]

View file

@ -11,5 +11,6 @@ mod path;
mod plugin_persistence;
#[cfg(feature = "plugin")]
mod plugins;
mod repl;
mod scope;
mod shell;

View file

@ -1,3 +1,5 @@
use rstest::rstest;
use nu_test_support::nu_with_plugins;
use pretty_assertions::assert_eq;
@ -190,3 +192,18 @@ fn generate_sequence() {
assert_eq!(actual.out, "[0,2,4,6,8,10]");
}
#[rstest]
#[timeout(std::time::Duration::from_secs(6))]
fn echo_interactivity_on_slow_pipelines() {
// This test works by putting 0 on the upstream immediately, followed by 1 after 10 seconds.
// If values aren't streamed to the plugin as they become available, `example echo` won't emit
// anything until both 0 and 1 are available. The desired behavior is that `example echo` gets
// the 0 immediately, which is consumed by `first`, allowing the pipeline to terminate early.
let actual = nu_with_plugins!(
cwd: "tests/fixtures/formats",
plugin: ("nu_plugin_example"),
r#"[1] | each { |n| sleep 10sec; $n } | prepend 0 | example echo | first"#
);
assert_eq!(actual.out, "0");
}

27
tests/repl/mod.rs Normal file
View file

@ -0,0 +1,27 @@
mod test_bits;
mod test_cell_path;
mod test_commandline;
mod test_conditionals;
mod test_config;
mod test_config_path;
mod test_converters;
mod test_custom_commands;
mod test_engine;
mod test_env;
mod test_help;
mod test_hiding;
mod test_ide;
mod test_iteration;
mod test_known_external;
mod test_math;
mod test_modules;
mod test_parser;
mod test_ranges;
mod test_regex;
mod test_signatures;
mod test_spread;
mod test_stdlib;
mod test_strings;
mod test_table_operations;
mod test_type_check;
mod tests;

View file

@ -1,4 +1,4 @@
use crate::tests::{run_test, TestResult};
use crate::repl::tests::{run_test, TestResult};
#[test]
fn bits_and() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
// Tests for null / null / Value::Nothing
#[test]

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn commandline_test_get_empty() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{run_test, TestResult};
use crate::repl::tests::{run_test, TestResult};
#[test]
fn if_test1() -> TestResult {

View file

@ -1,5 +1,4 @@
use super::{fail_test, run_test, run_test_std};
use crate::tests::TestResult;
use crate::repl::tests::{fail_test, run_test, run_test_std, TestResult};
#[test]
fn mutate_nu_config() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{run_test, TestResult};
use crate::repl::tests::{run_test, TestResult};
#[test]
fn from_json_1() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, run_test_contains, TestResult};
use crate::repl::tests::{fail_test, run_test, run_test_contains, TestResult};
use nu_test_support::nu;
use pretty_assertions::assert_eq;

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
use rstest::rstest;
#[test]

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
use nu_test_support::nu;
#[test]

View file

@ -1,4 +1,4 @@
use crate::tests::{run_test, TestResult};
use crate::repl::tests::{run_test, TestResult};
use rstest::rstest;
#[rstest]

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
// TODO: Test the use/hide tests also as separate lines in REPL (i.e., with merging the delta in between)
#[test]

View file

@ -1,4 +1,4 @@
use crate::tests::{test_ide_contains, TestResult};
use crate::repl::tests::{test_ide_contains, TestResult};
#[test]
fn parser_recovers() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{run_test, TestResult};
use crate::repl::tests::{run_test, TestResult};
#[test]
fn better_block_types() -> TestResult {

View file

@ -1,7 +1,6 @@
use crate::repl::tests::{fail_test, run_test, run_test_contains, TestResult};
use std::process::Command;
use crate::tests::{fail_test, run_test, run_test_contains, TestResult};
// cargo version prints a string of the form:
// cargo 1.60.0 (d1fd9fe2c 2022-03-01)

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn add_simple() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn module_def_imports_1() -> TestResult {

View file

@ -1,9 +1,7 @@
use crate::tests::{fail_test, run_test, run_test_with_env, TestResult};
use crate::repl::tests::{fail_test, run_test, run_test_contains, run_test_with_env, TestResult};
use nu_test_support::{nu, nu_repl_code};
use std::collections::HashMap;
use super::run_test_contains;
#[test]
fn env_shorthand() -> TestResult {
run_test("FOO=BAR if false { 3 } else { 4 }", "4")

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn int_in_inc_range() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn contains() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn list_annotations() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
use nu_test_support::nu;
#[test]

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test_std, TestResult};
use crate::repl::tests::{fail_test, run_test_std, TestResult};
#[test]
fn library_loaded() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn cjk_in_substrings() -> TestResult {
@ -154,6 +154,18 @@ fn raw_string_inside_closure() -> TestResult {
}
#[test]
fn incomplete_raw_string() -> TestResult {
fail_test("r#abc", "expected '")
fn incomplete_string() -> TestResult {
fail_test("r#abc", "expected '")?;
fail_test("r#'bc", "expected closing '#")?;
fail_test("'ab\"", "expected closing '")?;
fail_test("\"ab'", "expected closing \"")?;
fail_test(
r#"def func [] {
{
"A": ""B" # <- the quote is bad
}
}
"#,
"expected closing \"",
)
}

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn illegal_column_duplication() -> TestResult {

View file

@ -1,4 +1,4 @@
use crate::tests::{fail_test, run_test, TestResult};
use crate::repl::tests::{fail_test, run_test, TestResult};
#[test]
fn chained_operator_typecheck() -> TestResult {

View file

@ -1,30 +1,3 @@
mod test_bits;
mod test_cell_path;
mod test_commandline;
mod test_conditionals;
mod test_config;
mod test_config_path;
mod test_converters;
mod test_custom_commands;
mod test_engine;
mod test_env;
mod test_help;
mod test_hiding;
mod test_ide;
mod test_iteration;
mod test_known_external;
mod test_math;
mod test_modules;
mod test_parser;
mod test_ranges;
mod test_regex;
mod test_signatures;
mod test_spread;
mod test_stdlib;
mod test_strings;
mod test_table_operations;
mod test_type_check;
use assert_cmd::prelude::*;
use pretty_assertions::assert_eq;
use std::collections::HashMap;