Merge branch 'main' into path_insensitive

This commit is contained in:
Darren Schroeder 2024-11-20 13:36:13 -06:00
commit 31d3e37e3d
19 changed files with 536 additions and 184 deletions

16
Cargo.lock generated
View File

@ -3333,7 +3333,7 @@ dependencies = [
"sysinfo 0.32.0", "sysinfo 0.32.0",
"tabled", "tabled",
"tempfile", "tempfile",
"terminal_size 0.3.0", "terminal_size 0.4.0",
"titlecase", "titlecase",
"toml 0.8.19", "toml 0.8.19",
"trash", "trash",
@ -3378,7 +3378,7 @@ dependencies = [
"nu-path", "nu-path",
"nu-protocol", "nu-protocol",
"nu-utils", "nu-utils",
"terminal_size 0.3.0", "terminal_size 0.4.0",
] ]
[[package]] [[package]]
@ -3402,7 +3402,7 @@ dependencies = [
"nu-utils", "nu-utils",
"ratatui", "ratatui",
"strip-ansi-escapes", "strip-ansi-escapes",
"terminal_size 0.3.0", "terminal_size 0.4.0",
"unicode-width 0.1.11", "unicode-width 0.1.11",
] ]
@ -3485,7 +3485,7 @@ dependencies = [
"nu-protocol", "nu-protocol",
"nu-utils", "nu-utils",
"serde", "serde",
"thiserror 1.0.69", "thiserror 2.0.3",
"typetag", "typetag",
] ]
@ -3591,7 +3591,7 @@ dependencies = [
"strum", "strum",
"strum_macros", "strum_macros",
"tempfile", "tempfile",
"thiserror 1.0.69", "thiserror 2.0.3",
"typetag", "typetag",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -3635,7 +3635,7 @@ dependencies = [
"nu-protocol", "nu-protocol",
"nu-utils", "nu-utils",
"tabled", "tabled",
"terminal_size 0.3.0", "terminal_size 0.4.0",
] ]
[[package]] [[package]]
@ -5893,9 +5893,9 @@ dependencies = [
[[package]] [[package]]
name = "shadow-rs" name = "shadow-rs"
version = "0.35.2" version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b2328fb3ec0d5302f95915e7e77cfc2ff943714d9970bc4b66e9eacf318687" checksum = "58cfcd0643497a9f780502063aecbcc4a3212cbe4948fd25ee8fd179c2cf9a18"
dependencies = [ dependencies = [
"const_format", "const_format",
"is_debug", "is_debug",

View File

@ -156,7 +156,7 @@ syn = "2.0"
sysinfo = "0.32" sysinfo = "0.32"
tabled = { version = "0.16.0", default-features = false } tabled = { version = "0.16.0", default-features = false }
tempfile = "3.14" tempfile = "3.14"
terminal_size = "0.3" terminal_size = "0.4"
titlecase = "2.0" titlecase = "2.0"
toml = "0.8" toml = "0.8"
trash = "5.2" trash = "5.2"

View File

@ -74,7 +74,7 @@ pub fn evaluate_commands(
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err); report_compile_error(&working_set, err);
// Not a fatal error, for now std::process::exit(1);
} }
(output, working_set.render()) (output, working_set.render())

View File

@ -89,7 +89,7 @@ pub fn evaluate_file(
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err); report_compile_error(&working_set, err);
// Not a fatal error, for now std::process::exit(1);
} }
// Look for blocks whose name starts with "main" and replace it with the filename. // Look for blocks whose name starts with "main" and replace it with the filename.

View File

@ -296,7 +296,7 @@ fn evaluate_source(
if let Some(err) = working_set.compile_errors.first() { if let Some(err) = working_set.compile_errors.first() {
report_compile_error(&working_set, err); report_compile_error(&working_set, err);
// Not a fatal error, for now return Ok(true);
} }
(output, working_set.render()) (output, working_set.render())

View File

@ -21,10 +21,10 @@ nu-protocol = { path = "../nu-protocol", version = "0.100.1" }
nu-utils = { path = "../nu-utils", version = "0.100.1" } nu-utils = { path = "../nu-utils", version = "0.100.1" }
itertools = { workspace = true } itertools = { workspace = true }
shadow-rs = { version = "0.35", default-features = false } shadow-rs = { version = "0.36", default-features = false }
[build-dependencies] [build-dependencies]
shadow-rs = { version = "0.35", default-features = false } shadow-rs = { version = "0.36", default-features = false }
[features] [features]
mimalloc = [] mimalloc = []

View File

@ -1,6 +1,7 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_parser::parse; use nu_parser::{flatten_block, parse};
use nu_protocol::engine::StateWorkingSet; use nu_protocol::{engine::StateWorkingSet, record};
use serde_json::{json, Value as JsonValue};
#[derive(Clone)] #[derive(Clone)]
pub struct Ast; pub struct Ast;
@ -16,109 +17,23 @@ impl Command for Ast {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("ast") Signature::build("ast")
.input_output_types(vec![(Type::String, Type::record())]) .input_output_types(vec![
(Type::Nothing, Type::table()),
(Type::Nothing, Type::record()),
(Type::Nothing, Type::String),
])
.required( .required(
"pipeline", "pipeline",
SyntaxShape::String, SyntaxShape::String,
"The pipeline to print the ast for.", "The pipeline to print the ast for.",
) )
.switch("json", "serialize to json", Some('j')) .switch("json", "Serialize to json", Some('j'))
.switch("minify", "minify the nuon or json output", Some('m')) .switch("minify", "Minify the nuon or json output", Some('m'))
.switch("flatten", "An easier to read version of the ast", Some('f'))
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Debug) .category(Category::Debug)
} }
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
let to_json = call.has_flag(engine_state, stack, "json")?;
let minify = call.has_flag(engine_state, stack, "minify")?;
let mut working_set = StateWorkingSet::new(engine_state);
let block_output = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
let error_output = working_set.parse_errors.first();
let block_span = match &block_output.span {
Some(span) => span,
None => &pipeline.span,
};
if to_json {
// Get the block as json
let serde_block_str = if minify {
serde_json::to_string(&*block_output)
} else {
serde_json::to_string_pretty(&*block_output)
};
let block_json = match serde_block_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "block".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {block_output:?} to string"
)),
})?,
};
// Get the error as json
let serde_error_str = if minify {
serde_json::to_string(&error_output)
} else {
serde_json::to_string_pretty(&error_output)
};
let error_json = match serde_error_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "error".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {error_output:?} to string"
)),
})?,
};
// Create a new output record, merging the block and error
let output_record = Value::record(
record! {
"block" => Value::string(block_json, *block_span),
"error" => Value::string(error_json, Span::test_data()),
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
} else {
let block_value = Value::string(
if minify {
format!("{block_output:?}")
} else {
format!("{block_output:#?}")
},
pipeline.span,
);
let error_value = Value::string(
if minify {
format!("{error_output:?}")
} else {
format!("{error_output:#?}")
},
pipeline.span,
);
let output_record = Value::record(
record! {
"block" => block_value,
"error" => error_value
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
}
}
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
@ -147,8 +62,247 @@ impl Command for Ast {
example: "ast 'for x in 1..10 { echo $x ' --json --minify", example: "ast 'for x in 1..10 { echo $x ' --json --minify",
result: None, result: None,
}, },
Example {
description: "Print the ast of a string flattened",
example: r#"ast "'hello'" --flatten"#,
result: Some(Value::test_list(vec![Value::test_record(record! {
"content" => Value::test_string("'hello'"),
"shape" => Value::test_string("shape_string"),
"span" => Value::test_record(record! {
"start" => Value::test_int(0),
"end" => Value::test_int(7),}),
})])),
},
Example {
description: "Print the ast of a string flattened, as json, minified",
example: r#"ast "'hello'" --flatten --json --minify"#,
result: Some(Value::test_string(
r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#,
)),
},
Example {
description: "Print the ast of a pipeline flattened",
example: r#"ast 'ls | sort-by type name -i' --flatten"#,
result: Some(Value::test_list(vec![
Value::test_record(record! {
"content" => Value::test_string("ls"),
"shape" => Value::test_string("shape_external"),
"span" => Value::test_record(record! {
"start" => Value::test_int(0),
"end" => Value::test_int(2),}),
}),
Value::test_record(record! {
"content" => Value::test_string("|"),
"shape" => Value::test_string("shape_pipe"),
"span" => Value::test_record(record! {
"start" => Value::test_int(3),
"end" => Value::test_int(4),}),
}),
Value::test_record(record! {
"content" => Value::test_string("sort-by"),
"shape" => Value::test_string("shape_internalcall"),
"span" => Value::test_record(record! {
"start" => Value::test_int(5),
"end" => Value::test_int(12),}),
}),
Value::test_record(record! {
"content" => Value::test_string("type"),
"shape" => Value::test_string("shape_string"),
"span" => Value::test_record(record! {
"start" => Value::test_int(13),
"end" => Value::test_int(17),}),
}),
Value::test_record(record! {
"content" => Value::test_string("name"),
"shape" => Value::test_string("shape_string"),
"span" => Value::test_record(record! {
"start" => Value::test_int(18),
"end" => Value::test_int(22),}),
}),
Value::test_record(record! {
"content" => Value::test_string("-i"),
"shape" => Value::test_string("shape_flag"),
"span" => Value::test_record(record! {
"start" => Value::test_int(23),
"end" => Value::test_int(25),}),
}),
])),
},
] ]
} }
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
let to_json = call.has_flag(engine_state, stack, "json")?;
let minify = call.has_flag(engine_state, stack, "minify")?;
let flatten = call.has_flag(engine_state, stack, "flatten")?;
let mut working_set = StateWorkingSet::new(engine_state);
let offset = working_set.next_span_start();
let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
if flatten {
let flat = flatten_block(&working_set, &parsed_block);
if to_json {
let mut json_val: JsonValue = json!([]);
for (span, shape) in flat {
let content =
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let json = json!(
{
"content": content,
"shape": shape.to_string(),
"span": {
"start": span.start.checked_sub(offset),
"end": span.end.checked_sub(offset),
},
}
);
json_merge(&mut json_val, &json);
}
let json_string = if minify {
if let Ok(json_str) = serde_json::to_string(&json_val) {
json_str
} else {
"{}".to_string()
}
} else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) {
json_str
} else {
"{}".to_string()
};
Ok(Value::string(json_string, pipeline.span).into_pipeline_data())
} else {
// let mut rec: Record = Record::new();
let mut rec = vec![];
for (span, shape) in flat {
let content =
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let each_rec = record! {
"content" => Value::test_string(content),
"shape" => Value::test_string(shape.to_string()),
"span" => Value::test_record(record!{
"start" => Value::test_int(match span.start.checked_sub(offset) {
Some(start) => start as i64,
None => 0
}),
"end" => Value::test_int(match span.end.checked_sub(offset) {
Some(end) => end as i64,
None => 0
}),
}),
};
rec.push(Value::test_record(each_rec));
}
Ok(Value::list(rec, pipeline.span).into_pipeline_data())
}
} else {
let error_output = working_set.parse_errors.first();
let block_span = match &parsed_block.span {
Some(span) => span,
None => &pipeline.span,
};
if to_json {
// Get the block as json
let serde_block_str = if minify {
serde_json::to_string(&*parsed_block)
} else {
serde_json::to_string_pretty(&*parsed_block)
};
let block_json = match serde_block_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "block".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {parsed_block:?} to string"
)),
})?,
};
// Get the error as json
let serde_error_str = if minify {
serde_json::to_string(&error_output)
} else {
serde_json::to_string_pretty(&error_output)
};
let error_json = match serde_error_str {
Ok(json) => json,
Err(e) => Err(ShellError::CantConvert {
to_type: "string".to_string(),
from_type: "error".to_string(),
span: *block_span,
help: Some(format!(
"Error: {e}\nCan't convert {error_output:?} to string"
)),
})?,
};
// Create a new output record, merging the block and error
let output_record = Value::record(
record! {
"block" => Value::string(block_json, *block_span),
"error" => Value::string(error_json, Span::test_data()),
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
} else {
let block_value = Value::string(
if minify {
format!("{parsed_block:?}")
} else {
format!("{parsed_block:#?}")
},
pipeline.span,
);
let error_value = Value::string(
if minify {
format!("{error_output:?}")
} else {
format!("{error_output:#?}")
},
pipeline.span,
);
let output_record = Value::record(
record! {
"block" => block_value,
"error" => error_value
},
pipeline.span,
);
Ok(output_record.into_pipeline_data())
}
}
}
}
fn json_merge(a: &mut JsonValue, b: &JsonValue) {
match (a, b) {
(JsonValue::Object(ref mut a), JsonValue::Object(b)) => {
for (k, v) in b {
json_merge(a.entry(k).or_insert(JsonValue::Null), v);
}
}
(JsonValue::Array(ref mut a), JsonValue::Array(b)) => {
a.extend(b.clone());
}
(JsonValue::Array(ref mut a), JsonValue::Object(b)) => {
a.extend([JsonValue::Object(b.clone())]);
}
(a, b) => {
*a = b.clone();
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -5,6 +5,8 @@ use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDe
use nu_system::ForegroundChild; use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths; use pathdiff::diff_paths;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
@ -91,6 +93,22 @@ impl Command for External {
false false
}; };
// let's make sure it's a .ps1 script, but only on Windows
let potential_powershell_script = if cfg!(windows) {
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
let ext = executable
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
ext == "PS1"
} else {
false
}
} else {
false
};
// Find the absolute path to the executable. On Windows, set the // Find the absolute path to the executable. On Windows, set the
// executable to "cmd.exe" if it's a CMD internal command. If the // executable to "cmd.exe" if it's a CMD internal command. If the
// command is not found, display a helpful error message. // command is not found, display a helpful error message.
@ -98,11 +116,16 @@ impl Command for External {
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows) && (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
{ {
PathBuf::from("cmd.exe") PathBuf::from("cmd.exe")
} else if cfg!(windows) && potential_powershell_script {
// If we're on Windows and we're trying to run a PowerShell script, we'll use
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
// it's automatically installed on all modern windows systems.
PathBuf::from("powershell.exe")
} else { } else {
// Determine the PATH to be used and then use `which` to find it - though this has no // Determine the PATH to be used and then use `which` to find it - though this has no
// effect if it's an absolute path already // effect if it's an absolute path already
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else { let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
return Err(command_not_found(&name_str, call.head, engine_state, stack)); return Err(command_not_found(&name_str, call.head, engine_state, stack));
}; };
executable executable
@ -123,15 +146,29 @@ impl Command for External {
let args = eval_arguments_from_call(engine_state, stack, call)?; let args = eval_arguments_from_call(engine_state, stack, call)?;
#[cfg(windows)] #[cfg(windows)]
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows { if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
use std::os::windows::process::CommandExt;
// The /D flag disables execution of AutoRun commands from registry. // The /D flag disables execution of AutoRun commands from registry.
// The /C flag followed by a command name instructs CMD to execute // The /C flag followed by a command name instructs CMD to execute
// that command and quit. // that command and quit.
command.args(["/D", "/C", &name_str]); command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
for arg in &args { for arg in &args {
command.raw_arg(escape_cmd_argument(arg)?); command.raw_arg(escape_cmd_argument(arg)?);
} }
} else if potential_powershell_script {
use nu_path::canonicalize_with;
// canonicalize the path to the script so that tests pass
let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) {
canonicalize_with(&expanded_name, cwd)?
} else {
// If we can't get the current working directory, just provide the expanded name
expanded_name
};
// The -Command flag followed by a script name instructs PowerShell to
// execute that script and quit.
command.args(["-Command", &canon_path.to_string_lossy()]);
for arg in &args {
command.raw_arg(arg.item.clone());
}
} else { } else {
command.args(args.into_iter().map(|s| s.item)); command.args(args.into_iter().map(|s| s.item));
} }

View File

@ -44,8 +44,29 @@ fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list() let networks = Networks::new_with_refreshed_list()
.iter() .iter()
.map(|(iface, data)| { .map(|(iface, data)| {
let ip_addresses = data
.ip_networks()
.iter()
.map(|ip| {
let protocol = match ip.addr {
std::net::IpAddr::V4(_) => "ipv4",
std::net::IpAddr::V6(_) => "ipv6",
};
Value::record(
record! {
"address" => Value::string(ip.addr.to_string(), span),
"protocol" => Value::string(protocol, span),
"loop" => Value::bool(ip.addr.is_loopback(), span),
"multicast" => Value::bool(ip.addr.is_multicast(), span),
},
span,
)
})
.collect();
let record = record! { let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span), "name" => Value::string(trim_cstyle_null(iface), span),
"mac" => Value::string(data.mac_address().to_string(), span),
"ip" => Value::list(ip_addresses, span),
"sent" => Value::filesize(data.total_transmitted() as i64, span), "sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span), "recv" => Value::filesize(data.total_received() as i64, span),
}; };

View File

@ -1088,7 +1088,7 @@ fn create_empty_placeholder(
let data = vec![vec![cell]]; let data = vec![vec![cell]];
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_data_style(TextStyle::default().dimmed()); table.set_data_style(TextStyle::default().dimmed());
let out = TableOutput::new(table, false, false, false); let out = TableOutput::new(table, false, false, 1);
let style_computer = &StyleComputer::from_config(engine_state, stack); let style_computer = &StyleComputer::from_config(engine_state, stack);
let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default()); let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default());

View File

@ -355,9 +355,9 @@ fn external_command_receives_raw_binary_data() {
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files() { fn can_run_cmd_files() {
use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run a Windows batch file", |dirs, sandbox| { Playground::setup("run a Windows cmd file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent( sandbox.with_files(&[FileWithContent(
"foo.cmd", "foo.cmd",
r#" r#"
@ -371,12 +371,30 @@ fn can_run_batch_files() {
}); });
} }
#[cfg(windows)]
#[test]
fn can_run_batch_files() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run a Windows batch file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.bat",
r#"
@echo off
echo Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.bat"));
assert!(actual.out.contains("Hello World"));
});
}
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files_without_cmd_extension() { fn can_run_batch_files_without_cmd_extension() {
use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContent;
Playground::setup( Playground::setup(
"run a Windows batch file without specifying the extension", "run a Windows cmd file without specifying the extension",
|dirs, sandbox| { |dirs, sandbox| {
sandbox.with_files(&[FileWithContent( sandbox.with_files(&[FileWithContent(
"foo.cmd", "foo.cmd",
@ -440,3 +458,20 @@ fn redirect_combine() {
assert_eq!(actual.out, "FooBar"); assert_eq!(actual.out, "FooBar");
}); });
} }
#[cfg(windows)]
#[test]
fn can_run_ps1_files() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run_a_windows_ps_file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.ps1",
r#"
Write-Host Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.ps1"));
assert!(actual.out.contains("Hello World"));
});
}

View File

@ -2941,3 +2941,123 @@ fn table_footer_inheritance() {
assert_eq!(actual.out.match_indices("x2").count(), 1); assert_eq!(actual.out.match_indices("x2").count(), 1);
assert_eq!(actual.out.match_indices("x3").count(), 1); assert_eq!(actual.out.match_indices("x3").count(), 1);
} }
#[test]
fn table_footer_inheritance_kv_rows() {
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────╮\
# a b \
\
0 kv \
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
\
1 data 0 \
2 data 0 \
"
);
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────╮\
# a b \
\
0 kv \
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
5 5 \
\
1 data 0 \
2 data 0 \
\
# a b \
"
);
}
#[test]
fn table_footer_inheritance_list_rows() {
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────────────────╮\
# a b \
\
0 kv \
\
0 # field \
\
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
\
\
1 data 0 \
2 data 0 \
"
);
let actual = nu!(
concat!(
"$env.config.table.footer_inheritance = true;",
"$env.config.footer_mode = 7;",
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4] [5]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
)
);
assert_eq!(
actual.out,
"╭───┬──────┬───────────────────────╮\
# a b \
\
0 kv \
\
0 # field \
\
0 0 \
1 1 \
2 2 \
3 3 \
4 4 \
5 5 \
\
\
1 data 0 \
2 data 0 \
\
# a b \
"
);
}

View File

@ -21,7 +21,7 @@ nu-plugin-core = { path = "../nu-plugin-core", version = "0.100.1", default-feat
nu-utils = { path = "../nu-utils", version = "0.100.1" } nu-utils = { path = "../nu-utils", version = "0.100.1" }
log = { workspace = true } log = { workspace = true }
thiserror = "1.0" thiserror = "2.0"
[dev-dependencies] [dev-dependencies]
serde = { workspace = true } serde = { workspace = true }

View File

@ -36,7 +36,7 @@ num-format = { workspace = true }
rmp-serde = { workspace = true, optional = true } rmp-serde = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = "1.0" thiserror = "2.0"
typetag = "0.2" typetag = "0.2"
os_pipe = { workspace = true, features = ["io_safety"] } os_pipe = { workspace = true, features = ["io_safety"] }
log = { workspace = true } log = { workspace = true }

View File

@ -18,8 +18,12 @@ pub fn create_nu_table_config(
expand: bool, expand: bool,
mode: TableMode, mode: TableMode,
) -> NuTableConfig { ) -> NuTableConfig {
let with_footer = (config.table.footer_inheritance && out.with_footer) let mut count_rows = out.table.count_rows();
|| with_footer(config, out.with_header, out.table.count_rows()); if config.table.footer_inheritance {
count_rows = out.count_rows;
}
let with_footer = with_footer(config, out.with_header, count_rows);
NuTableConfig { NuTableConfig {
theme: load_theme(mode), theme: load_theme(mode),

View File

@ -615,12 +615,15 @@ fn load_theme(
if let Some(style) = sep_color { if let Some(style) = sep_color {
let color = convert_style(style); let color = convert_style(style);
let color = ANSIBuf::from(color); let color = ANSIBuf::from(color);
// todo: use .modify(Segment::all(), color) --> it has this optimization
table.get_config_mut().set_border_color_default(color); table.get_config_mut().set_border_color_default(color);
} }
if !with_header { if !with_header {
// todo: remove and use theme.remove_horizontal_lines();
table.with(RemoveHorizontalLine); table.with(RemoveHorizontalLine);
} else if with_footer { } else if with_footer {
// todo: remove and set it on theme rather then here...
table.with(CopyFirstHorizontalLineAtLast); table.with(CopyFirstHorizontalLineAtLast);
} }
} }
@ -1257,6 +1260,7 @@ fn remove_row(recs: &mut NuRecords, row: usize) -> Vec<String> {
columns columns
} }
// todo; use Format?
struct StripColorFromRow(usize); struct StripColorFromRow(usize);
impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for StripColorFromRow { impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for StripColorFromRow {

View File

@ -5,7 +5,7 @@ use crate::{
NuText, StringResult, TableResult, INDEX_COLUMN_NAME, NuText, StringResult, TableResult, INDEX_COLUMN_NAME,
}, },
string_width, string_width,
types::{has_footer, has_index}, types::has_index,
NuTable, NuTableCell, TableOpts, TableOutput, NuTable, NuTableCell, TableOpts, TableOutput,
}; };
use nu_color_config::{Alignment, StyleComputer, TextStyle}; use nu_color_config::{Alignment, StyleComputer, TextStyle};
@ -63,22 +63,22 @@ struct Cfg<'a> {
struct CellOutput { struct CellOutput {
text: String, text: String,
style: TextStyle, style: TextStyle,
is_big: bool, size: usize,
is_expanded: bool, is_expanded: bool,
} }
impl CellOutput { impl CellOutput {
fn new(text: String, style: TextStyle, is_big: bool, is_expanded: bool) -> Self { fn new(text: String, style: TextStyle, size: usize, is_expanded: bool) -> Self {
Self { Self {
text, text,
style, style,
is_big, size,
is_expanded, is_expanded,
} }
} }
fn clean(text: String, is_big: bool, is_expanded: bool) -> Self { fn clean(text: String, size: usize, is_expanded: bool) -> Self {
Self::new(text, Default::default(), is_big, is_expanded) Self::new(text, Default::default(), size, is_expanded)
} }
fn text(text: String) -> Self { fn text(text: String) -> Self {
@ -86,7 +86,7 @@ impl CellOutput {
} }
fn styled(text: NuText) -> Self { fn styled(text: NuText) -> Self {
Self::new(text.0, text.1, false, false) Self::new(text.0, text.1, 1, false)
} }
} }
@ -117,7 +117,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
let with_index = has_index(&cfg.opts, &headers); let with_index = has_index(&cfg.opts, &headers);
let row_offset = cfg.opts.index_offset; let row_offset = cfg.opts.index_offset;
let mut is_footer_used = false; let mut rows_count = 0usize;
// The header with the INDEX is removed from the table headers since // The header with the INDEX is removed from the table headers since
// it is added to the natural table index // it is added to the natural table index
@ -199,9 +199,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
data[row].push(value); data[row].push(value);
data_styles.insert((row, with_index as usize), cell.style); data_styles.insert((row, with_index as usize), cell.style);
if cell.is_big { rows_count = rows_count.saturating_add(cell.size);
is_footer_used = cell.is_big;
}
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
@ -209,12 +207,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
table.set_index_style(get_index_style(cfg.opts.style_computer)); table.set_index_style(get_index_style(cfg.opts.style_computer));
set_data_styles(&mut table, data_styles); set_data_styles(&mut table, data_styles);
return Ok(Some(TableOutput::new( return Ok(Some(TableOutput::new(table, false, with_index, rows_count)));
table,
false,
with_index,
is_footer_used,
)));
} }
if !headers.is_empty() { if !headers.is_empty() {
@ -269,6 +262,8 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
} }
} }
let mut column_rows = 0usize;
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?; cfg.opts.signals.check(cfg.opts.span)?;
@ -294,9 +289,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
data[row + 1].push(value); data[row + 1].push(value);
data_styles.insert((row + 1, col + with_index as usize), cell.style); data_styles.insert((row + 1, col + with_index as usize), cell.style);
if cell.is_big { column_rows = column_rows.saturating_add(cell.size);
is_footer_used = cell.is_big;
}
} }
let head_cell = NuTableCell::new(header); let head_cell = NuTableCell::new(header);
@ -316,6 +309,8 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
available_width -= pad_space + column_width; available_width -= pad_space + column_width;
rendered_column += 1; rendered_column += 1;
rows_count = std::cmp::max(rows_count, column_rows);
} }
if truncate && rendered_column == 0 { if truncate && rendered_column == 0 {
@ -374,9 +369,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1);
set_data_styles(&mut table, data_styles); set_data_styles(&mut table, data_styles);
let has_footer = is_footer_used || has_footer(&cfg.opts, table.count_rows() as u64); Ok(Some(TableOutput::new(table, true, with_index, rows_count)))
Ok(Some(TableOutput::new(table, true, with_index, has_footer)))
} }
fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult { fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
@ -395,7 +388,7 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
let value_width = cfg.opts.width - key_width - count_borders - padding - padding; let value_width = cfg.opts.width - key_width - count_borders - padding - padding;
let mut with_footer = false; let mut count_rows = 0usize;
let mut data = Vec::with_capacity(record.len()); let mut data = Vec::with_capacity(record.len());
for (key, value) in record { for (key, value) in record {
@ -420,19 +413,17 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
data.push(row); data.push(row);
if cell.is_big { count_rows = count_rows.saturating_add(cell.size);
with_footer = cell.is_big;
}
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(get_key_style(&cfg)); table.set_index_style(get_key_style(&cfg));
table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1);
let out = TableOutput::new(table, false, true, with_footer); let out = TableOutput::new(table, false, true, count_rows);
maybe_expand_table(out, cfg.opts.width, &cfg.opts) maybe_expand_table(out, cfg.opts.width, &cfg.opts)
.map(|value| value.map(|value| CellOutput::clean(value, with_footer, false))) .map(|value| value.map(|value| CellOutput::clean(value, count_rows, false)))
} }
// the flag is used as an optimization to not do `value.lines().count()` search. // the flag is used as an optimization to not do `value.lines().count()` search.
@ -441,7 +432,7 @@ fn expand_table_value(value: &Value, value_width: usize, cfg: &Cfg<'_>) -> CellR
if is_limited { if is_limited {
return Ok(Some(CellOutput::clean( return Ok(Some(CellOutput::clean(
value_to_string_clean(value, cfg), value_to_string_clean(value, cfg),
false, 1,
false, false,
))); )));
} }
@ -457,7 +448,7 @@ fn expand_table_value(value: &Value, value_width: usize, cfg: &Cfg<'_>) -> CellR
let cfg = create_table_cfg(cfg, &out); let cfg = create_table_cfg(cfg, &out);
let value = out.table.draw(cfg, value_width); let value = out.table.draw(cfg, value_width);
match value { match value {
Some(value) => Ok(Some(CellOutput::clean(value, out.with_footer, true))), Some(value) => Ok(Some(CellOutput::clean(value, out.count_rows, true))),
None => Ok(None), None => Ok(None),
} }
} }
@ -484,7 +475,7 @@ fn expand_table_value(value: &Value, value_width: usize, cfg: &Cfg<'_>) -> CellR
let inner_cfg = update_config(dive_options(cfg, span), value_width); let inner_cfg = update_config(dive_options(cfg, span), value_width);
let result = expanded_table_kv(record, inner_cfg)?; let result = expanded_table_kv(record, inner_cfg)?;
match result { match result {
Some(result) => Ok(Some(CellOutput::clean(result.text, result.is_big, true))), Some(result) => Ok(Some(CellOutput::clean(result.text, result.size, true))),
None => Ok(Some(CellOutput::text(value_to_wrapped_string( None => Ok(Some(CellOutput::text(value_to_wrapped_string(
value, value,
cfg, cfg,
@ -575,7 +566,7 @@ fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> CellOutput {
let table_config = create_table_cfg(&cfg, &out); let table_config = create_table_cfg(&cfg, &out);
let table = out.table.draw(table_config, usize::MAX); let table = out.table.draw(table_config, usize::MAX);
match table { match table {
Some(table) => CellOutput::clean(table, out.with_footer, false), Some(table) => CellOutput::clean(table, out.count_rows, false),
None => CellOutput::styled(nu_value_to_string( None => CellOutput::styled(nu_value_to_string(
item, item,
cfg.opts.config, cfg.opts.config,

View File

@ -56,8 +56,9 @@ fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(TextStyle::default_field()); table.set_index_style(TextStyle::default_field());
let count_rows = table.count_rows();
let mut out = TableOutput::new(table, false, true, false); let mut out = TableOutput::new(table, false, true, count_rows);
let left = opts.config.table.padding.left; let left = opts.config.table.padding.left;
let right = opts.config.table.padding.right; let right = opts.config.table.padding.right;
@ -82,7 +83,10 @@ fn table(input: &[Value], opts: &TableOpts<'_>) -> TableResult {
let with_header = !headers.is_empty(); let with_header = !headers.is_empty();
if !with_header { if !with_header {
let table = to_table_with_no_header(input, with_index, row_offset, opts)?; let table = to_table_with_no_header(input, with_index, row_offset, opts)?;
let table = table.map(|table| TableOutput::new(table, false, with_index, false)); let table = table.map(|table| {
let count_rows = table.count_rows();
TableOutput::new(table, false, with_index, count_rows)
});
return Ok(table); return Ok(table);
} }
@ -98,7 +102,10 @@ fn table(input: &[Value], opts: &TableOpts<'_>) -> TableResult {
.collect(); .collect();
let table = to_table_with_header(input, &headers, with_index, row_offset, opts)?; let table = to_table_with_header(input, &headers, with_index, row_offset, opts)?;
let table = table.map(|table| TableOutput::new(table, true, with_index, false)); let table = table.map(|table| {
let count_rows = table.count_rows();
TableOutput::new(table, true, with_index, count_rows)
});
Ok(table) Ok(table)
} }

View File

@ -1,8 +1,7 @@
use terminal_size::{terminal_size, Height, Width}; use nu_color_config::StyleComputer;
use nu_protocol::{Config, Signals, Span, TableIndexMode, TableMode};
use crate::{common::INDEX_COLUMN_NAME, NuTable}; use crate::{common::INDEX_COLUMN_NAME, NuTable};
use nu_color_config::StyleComputer;
use nu_protocol::{Config, FooterMode, Signals, Span, TableIndexMode, TableMode};
mod collapse; mod collapse;
mod expanded; mod expanded;
@ -16,16 +15,16 @@ pub struct TableOutput {
pub table: NuTable, pub table: NuTable,
pub with_header: bool, pub with_header: bool,
pub with_index: bool, pub with_index: bool,
pub with_footer: bool, pub count_rows: usize,
} }
impl TableOutput { impl TableOutput {
pub fn new(table: NuTable, with_header: bool, with_index: bool, with_footer: bool) -> Self { pub fn new(table: NuTable, with_header: bool, with_index: bool, count_rows: usize) -> Self {
Self { Self {
table, table,
with_header, with_header,
with_index, with_index,
with_footer, count_rows,
} }
} }
} }
@ -79,23 +78,3 @@ fn has_index(opts: &TableOpts<'_>, headers: &[String]) -> bool {
with_index && !opts.index_remove with_index && !opts.index_remove
} }
fn has_footer(opts: &TableOpts<'_>, count_records: u64) -> bool {
match opts.config.footer_mode {
// Only show the footer if there are more than RowCount rows
FooterMode::RowCount(limit) => count_records > limit,
// Always show the footer
FooterMode::Always => true,
// Never show the footer
FooterMode::Never => false,
// Calculate the screen height and row count, if screen height is larger than row count, don't show footer
FooterMode::Auto => {
let (_width, height) = match terminal_size() {
Some((w, h)) => (Width(w.0).0 as u64, Height(h.0).0 as u64),
None => (Width(0).0 as u64, Height(0).0 as u64),
};
height <= count_records
}
}
}