relocate debug commands (#8071)

# Description

Now that we've landed the debug commands we were working on, let's
relocate them to an easier place to find all of them. That's what this
PR does.

The only actual code change was changing the `timeit` command to a
`Category::Debug` command. The rest is just moving things around and
hooking them up.

# User-Facing Changes



# Tests + Formatting

Don't forget to add tests that cover your changes.

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 -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
Darren Schroeder
2023-02-13 10:39:07 -06:00
committed by GitHub
parent 8136170431
commit 4c787af26d
18 changed files with 40 additions and 34 deletions

View File

@ -0,0 +1,101 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Type, Value};
#[derive(Clone)]
pub struct Debug;
impl Command for Debug {
fn name(&self) -> &str {
"debug"
}
fn usage(&self) -> &str {
"Debug print the value(s) piped in."
}
fn signature(&self) -> Signature {
Signature::build("debug")
.input_output_types(vec![
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::String)),
),
(Type::Table(vec![]), Type::List(Box::new(Type::String))),
(Type::Any, Type::String),
])
.category(Category::Debug)
.switch("raw", "Prints the raw value representation", Some('r'))
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let config = engine_state.get_config().clone();
let raw = call.has_flag("raw");
// Should PipelineData::Empty result in an error here?
input.map(
move |x| {
if raw {
Value::String {
val: x.debug_value(),
span: head,
}
} else {
Value::String {
val: x.debug_string(", ", &config),
span: head,
}
}
},
engine_state.ctrlc.clone(),
)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Debug print a string",
example: "'hello' | debug",
result: Some(Value::test_string("hello")),
},
Example {
description: "Debug print a list",
example: "['hello'] | debug",
result: Some(Value::List {
vals: vec![Value::test_string("hello")],
span: Span::test_data(),
}),
},
Example {
description: "Debug print a table",
example: "[[version patch]; [0.1.0 false] [0.1.1 true] [0.2.0 false]] | debug",
result: Some(Value::List {
vals: vec![
Value::test_string("{version: 0.1.0, patch: false}"),
Value::test_string("{version: 0.1.1, patch: true}"),
Value::test_string("{version: 0.2.0, patch: false}"),
],
span: Span::test_data(),
}),
},
]
}
}
#[cfg(test)]
mod test {
#[test]
fn test_examples() {
use super::Debug;
use crate::test_examples;
test_examples(Debug {})
}
}

View File

@ -0,0 +1,330 @@
use nu_engine::{eval_expression, CallExt};
use nu_protocol::ast::{Argument, Block, Call, Expr, Expression};
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
SyntaxShape, Type, Value,
};
#[derive(Clone)]
pub struct Explain;
impl Command for Explain {
fn name(&self) -> &str {
"explain"
}
fn usage(&self) -> &str {
"Explain closure contents."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("explain")
.required(
"closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
"the closure to run",
)
.input_output_types(vec![(Type::Any, Type::Any), (Type::Nothing, Type::Any)])
.allow_variants_without_examples(true)
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
// This was all delightfully stolen from benchmark :)
let capture_block: Closure = call.req(engine_state, stack, 0)?;
let block = engine_state.get_block(capture_block.block_id);
let ctrlc = engine_state.ctrlc.clone();
let mut stack = stack.captures_to_stack(&capture_block.captures);
let elements = get_pipeline_elements(engine_state, &mut stack, block)?;
Ok(elements.into_pipeline_data(ctrlc))
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Explain a command within a closure",
example: "explain { ls | sort-by name type -i | get name } | table -e",
result: None,
}]
}
}
pub fn get_pipeline_elements(
engine_state: &EngineState,
stack: &mut Stack,
block: &Block,
) -> Result<Vec<Value>, ShellError> {
let mut element_values = vec![];
let span = Span::test_data();
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
let mut i = 0;
while i < pipeline.elements.len() {
let pipeline_element = &pipeline.elements[i];
let pipeline_expression = pipeline_element.expression().clone();
let pipeline_span = &pipeline_element.span();
let element_str =
String::from_utf8_lossy(engine_state.get_span_contents(pipeline_span));
let value = Value::string(element_str.to_string(), *pipeline_span);
let expr = pipeline_expression.expr.clone();
let (command_name, command_args_value) = if let Expr::Call(call) = expr {
let command = engine_state.get_decl(call.decl_id);
(
command.name().to_string(),
get_arguments(engine_state, stack, *call),
)
} else {
("no-op".to_string(), vec![])
};
let index = format!("{pipeline_idx}_{i}");
let value_type = value.get_type();
let value_span = value.span()?;
let value_span_start = value_span.start as i64;
let value_span_end = value_span.end as i64;
let command_name = command_name;
let rec = Value::Record {
cols: vec![
"cmd_index".to_string(),
"cmd_name".to_string(),
"type".to_string(),
"cmd_args".to_string(),
"span_start".to_string(),
"span_end".to_string(),
],
vals: vec![
Value::string(index, span),
Value::string(command_name, value_span),
Value::string(value_type.to_string(), span),
Value::List {
vals: command_args_value,
span: value_span,
},
Value::int(value_span_start, span),
Value::int(value_span_end, span),
],
span: value_span,
};
element_values.push(rec);
i += 1;
}
}
Ok(element_values)
}
fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> Vec<Value> {
let mut arg_value = vec![];
let span = Span::test_data();
for arg in &call.arguments {
match arg {
// I think the second argument to Argument::Named is the short name, but I'm not really sure.
// Please fix it if it's wrong. :)
Argument::Named((name, short, opt_expr)) => {
let arg_type = "named";
let arg_value_name = name.item.clone();
let arg_value_name_span_start = name.span.start as i64;
let arg_value_name_span_end = name.span.end as i64;
let rec = Value::Record {
cols: vec![
"arg_type".to_string(),
"name".to_string(),
"type".to_string(),
"span_start".to_string(),
"span_end".to_string(),
],
vals: vec![
Value::string(arg_type, span),
Value::string(arg_value_name, name.span),
Value::string("string".to_string(), span),
Value::int(arg_value_name_span_start, span),
Value::int(arg_value_name_span_end, span),
],
span: name.span,
};
arg_value.push(rec);
if let Some(shortcut) = short {
let arg_type = "short";
let arg_value_name = shortcut.item.clone();
let arg_value_name_span_start = shortcut.span.start as i64;
let arg_value_name_span_end = shortcut.span.end as i64;
let rec = Value::Record {
cols: vec![
"arg_type".to_string(),
"name".to_string(),
"type".to_string(),
"span_start".to_string(),
"span_end".to_string(),
],
vals: vec![
Value::string(arg_type, span),
Value::string(arg_value_name, shortcut.span),
Value::string("string".to_string(), span),
Value::int(arg_value_name_span_start, span),
Value::int(arg_value_name_span_end, span),
],
span: name.span,
};
arg_value.push(rec);
} else {
};
if let Some(expression) = opt_expr {
let evaluated_expression =
get_expression_as_value(engine_state, stack, expression);
let arg_type = "expr";
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.expect_span();
let arg_value_name_span_start = evaled_span.start as i64;
let arg_value_name_span_end = evaled_span.end as i64;
let rec = Value::Record {
cols: vec![
"arg_type".to_string(),
"name".to_string(),
"type".to_string(),
"span_start".to_string(),
"span_end".to_string(),
],
vals: vec![
Value::string(arg_type, span),
Value::string(arg_value_name, expression.span),
Value::string(arg_value_type, span),
Value::int(arg_value_name_span_start, span),
Value::int(arg_value_name_span_end, span),
],
span: expression.span,
};
arg_value.push(rec);
} else {
};
}
Argument::Positional(inner_expr) => {
let arg_type = "positional";
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.expect_span();
let arg_value_name_span_start = evaled_span.start as i64;
let arg_value_name_span_end = evaled_span.end as i64;
let rec = Value::Record {
cols: vec![
"arg_type".to_string(),
"name".to_string(),
"type".to_string(),
"span_start".to_string(),
"span_end".to_string(),
],
vals: vec![
Value::string(arg_type, span),
Value::string(arg_value_name, inner_expr.span),
Value::string(arg_value_type, span),
Value::int(arg_value_name_span_start, span),
Value::int(arg_value_name_span_end, span),
],
span: inner_expr.span,
};
arg_value.push(rec);
}
Argument::Unknown(inner_expr) => {
let arg_type = "unknown";
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.expect_span();
let arg_value_name_span_start = evaled_span.start as i64;
let arg_value_name_span_end = evaled_span.end as i64;
let rec = Value::Record {
cols: vec![
"arg_type".to_string(),
"name".to_string(),
"type".to_string(),
"span_start".to_string(),
"span_end".to_string(),
],
vals: vec![
Value::string(arg_type, span),
Value::string(arg_value_name, inner_expr.span),
Value::string(arg_value_type, span),
Value::int(arg_value_name_span_start, span),
Value::int(arg_value_name_span_end, span),
],
span: inner_expr.span,
};
arg_value.push(rec);
}
};
}
arg_value
}
fn get_expression_as_value(
engine_state: &EngineState,
stack: &mut Stack,
inner_expr: &Expression,
) -> Value {
match eval_expression(engine_state, stack, inner_expr) {
Ok(v) => v,
Err(error) => Value::Error { error },
}
}
pub fn debug_string_without_formatting(value: &Value) -> String {
match value {
Value::Bool { val, .. } => val.to_string(),
Value::Int { val, .. } => val.to_string(),
Value::Float { val, .. } => val.to_string(),
Value::Filesize { val, .. } => val.to_string(),
Value::Duration { val, .. } => val.to_string(),
Value::Date { val, .. } => format!("{val:?}"),
Value::Range { val, .. } => {
format!(
"{}..{}",
debug_string_without_formatting(&val.from),
debug_string_without_formatting(&val.to)
)
}
Value::String { val, .. } => val.clone(),
Value::List { vals: val, .. } => format!(
"[{}]",
val.iter()
.map(debug_string_without_formatting)
.collect::<Vec<_>>()
.join(" ")
),
Value::Record { cols, vals, .. } => format!(
"{{{}}}",
cols.iter()
.zip(vals.iter())
.map(|(x, y)| format!("{}: {}", x, debug_string_without_formatting(y)))
.collect::<Vec<_>>()
.join(" ")
),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => debug_string_without_formatting(&val),
Err(error) => format!("{error:?}"),
},
//TODO: It would be good to drill in deeper to blocks and closures.
Value::Block { val, .. } => format!("<Block {val}>"),
Value::Closure { val, .. } => format!("<Closure {val}>"),
Value::Nothing { .. } => String::new(),
Value::Error { error } => format!("{error:?}"),
Value::Binary { val, .. } => format!("{val:?}"),
Value::CellPath { val, .. } => val.into_string(),
Value::CustomValue { val, .. } => val.value_string(),
}
}

View File

@ -0,0 +1,64 @@
use super::inspect_table;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
use terminal_size::{terminal_size, Height, Width};
#[derive(Clone)]
pub struct Inspect;
impl Command for Inspect {
fn name(&self) -> &str {
"inspect"
}
fn usage(&self) -> &str {
"Inspect pipeline results while running a pipeline"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("inspect")
.input_output_types(vec![(Type::Any, Type::Any)])
.allow_variants_without_examples(true)
.category(Category::Debug)
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let input_metadata = input.metadata();
let input_val = input.into_value(call.head);
let original_input = input_val.clone();
let description = match input_val {
Value::CustomValue { ref val, .. } => val.value_string(),
_ => input_val.get_type().to_string(),
};
let (cols, _rows) = match terminal_size() {
Some((w, h)) => (Width(w.0), Height(h.0)),
None => (Width(0), Height(0)),
};
let table = inspect_table::build_table(input_val, description, cols.0 as usize);
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
// tabular output. If we printed to stdout, nushell would get confused with two outputs.
eprintln!("{table}\n");
Ok(original_input.into_pipeline_data_with_metadata(input_metadata))
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Inspect pipeline results",
example: "ls | inspect | get name | inspect",
result: None,
}]
}
}

View File

@ -0,0 +1,465 @@
use nu_protocol::Value;
use tabled::{
builder::Builder,
peaker::PriorityMax,
width::{MinWidth, Wrap},
Style,
};
use self::{
global_horizontal_char::SetHorizontalChar, peak2::Peak2, table_column_width::GetColumnWidths,
truncate_table::TruncateTable, width_increase::IncWidth,
};
pub fn build_table(value: Value, description: String, termsize: usize) -> String {
let (head, mut data) = util::collect_input(value);
data.insert(0, head);
let mut val_table = Builder::from(data).build();
let val_table_width = val_table.total_width();
let desc = vec![vec![String::from("description"), description]];
let mut desc_table = Builder::from(desc).build();
let desc_table_width = desc_table.total_width();
let width = val_table_width.clamp(desc_table_width, termsize);
desc_table
.with(Style::rounded().off_bottom())
.with(Wrap::new(width).priority::<PriorityMax>())
.with(MinWidth::new(width).priority::<Peak2>());
val_table
.with(Style::rounded().top_left_corner('├').top_right_corner('┤'))
.with(TruncateTable(width))
.with(Wrap::new(width).priority::<PriorityMax>())
.with(IncWidth(width));
let mut desc_widths = GetColumnWidths(Vec::new());
desc_table.with(&mut desc_widths);
val_table.with(SetHorizontalChar::new('┼', '┴', 0, desc_widths.0[0]));
format!("{desc_table}\n{val_table}")
}
mod truncate_table {
use tabled::{
papergrid::{
records::{Records, RecordsMut, Resizable},
width::{CfgWidthFunction, WidthEstimator},
Estimate,
},
TableOption,
};
pub struct TruncateTable(pub usize);
impl<R> TableOption<R> for TruncateTable
where
R: Records + RecordsMut<String> + Resizable,
{
fn change(&mut self, table: &mut tabled::Table<R>) {
let width = table.total_width();
if width <= self.0 {
return;
}
let count_columns = table.get_records().count_columns();
if count_columns < 1 {
return;
}
let mut evaluator = WidthEstimator::default();
evaluator.estimate(table.get_records(), table.get_config());
let columns_width: Vec<_> = evaluator.into();
const SPLIT_LINE_WIDTH: usize = 1;
let mut width = 0;
let mut i = 0;
for w in columns_width {
width += w + SPLIT_LINE_WIDTH;
if width >= self.0 {
break;
}
i += 1;
}
if i == 0 && count_columns > 0 {
i = 1;
} else if i + 1 == count_columns {
// we want to left at least 1 column
i -= 1;
}
let count_columns = table.get_records().count_columns();
let y = count_columns - i;
let mut column = count_columns;
for _ in 0..y {
column -= 1;
table.get_records_mut().remove_column(column);
}
table.get_records_mut().push_column();
let width_ctrl = CfgWidthFunction::from_cfg(table.get_config());
let last_column = table.get_records().count_columns() - 1;
for row in 0..table.get_records().count_rows() {
table
.get_records_mut()
.set((row, last_column), String::from(""), &width_ctrl)
}
}
}
}
mod util {
use crate::debug::explain::debug_string_without_formatting;
use nu_engine::get_columns;
use nu_protocol::{ast::PathMember, Span, Value};
/// Try to build column names and a table grid.
pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<String>>) {
match value {
Value::Record { cols, vals, .. } => (
cols,
vec![vals
.into_iter()
.map(|s| debug_string_without_formatting(&s))
.collect()],
),
Value::List { vals, .. } => {
let mut columns = get_columns(&vals);
let data = convert_records_to_dataset(&columns, vals);
if columns.is_empty() && !data.is_empty() {
columns = vec![String::from("")];
}
(columns, data)
}
Value::String { val, span } => {
let lines = val
.lines()
.map(|line| Value::String {
val: line.to_string(),
span,
})
.map(|val| vec![debug_string_without_formatting(&val)])
.collect();
(vec![String::from("")], lines)
}
Value::Nothing { .. } => (vec![], vec![]),
value => (
vec![String::from("")],
vec![vec![debug_string_without_formatting(&value)]],
),
}
}
fn convert_records_to_dataset(cols: &Vec<String>, records: Vec<Value>) -> Vec<Vec<String>> {
if !cols.is_empty() {
create_table_for_record(cols, &records)
} else if cols.is_empty() && records.is_empty() {
vec![]
} else if cols.len() == records.len() {
vec![records
.into_iter()
.map(|s| debug_string_without_formatting(&s))
.collect()]
} else {
records
.into_iter()
.map(|record| vec![debug_string_without_formatting(&record)])
.collect()
}
}
fn create_table_for_record(headers: &[String], items: &[Value]) -> Vec<Vec<String>> {
let mut data = vec![Vec::new(); items.len()];
for (i, item) in items.iter().enumerate() {
let row = record_create_row(headers, item);
data[i] = row;
}
data
}
fn record_create_row(headers: &[String], item: &Value) -> Vec<String> {
let mut rows = vec![String::default(); headers.len()];
for (i, header) in headers.iter().enumerate() {
let value = record_lookup_value(item, header);
rows[i] = debug_string_without_formatting(&value);
}
rows
}
fn record_lookup_value(item: &Value, header: &str) -> Value {
match item {
Value::Record { .. } => {
let path = PathMember::String {
val: header.to_owned(),
span: Span::unknown(),
};
item.clone()
.follow_cell_path(&[path], false, false)
.unwrap_or_else(|_| item.clone())
}
item => item.clone(),
}
}
}
mod style_no_left_right_1st {
use tabled::{papergrid::records::Records, Table, TableOption};
struct StyleOffLeftRightFirstLine;
impl<R> TableOption<R> for StyleOffLeftRightFirstLine
where
R: Records,
{
fn change(&mut self, table: &mut Table<R>) {
let shape = table.shape();
let cfg = table.get_config_mut();
let mut b = cfg.get_border((0, 0), shape);
b.left = Some(' ');
cfg.set_border((0, 0), b);
let mut b = cfg.get_border((0, shape.1 - 1), shape);
b.right = Some(' ');
cfg.set_border((0, 0), b);
}
}
}
mod peak2 {
use tabled::peaker::Peaker;
pub struct Peak2;
impl Peaker for Peak2 {
fn create() -> Self {
Self
}
fn peak(&mut self, _: &[usize], _: &[usize]) -> Option<usize> {
Some(1)
}
}
}
mod table_column_width {
use tabled::papergrid::{records::Records, Estimate};
pub struct GetColumnWidths(pub Vec<usize>);
impl<R> tabled::TableOption<R> for GetColumnWidths
where
R: Records,
{
fn change(&mut self, table: &mut tabled::Table<R>) {
let mut evaluator = tabled::papergrid::width::WidthEstimator::default();
evaluator.estimate(table.get_records(), table.get_config());
self.0 = evaluator.into();
}
}
}
mod global_horizontal_char {
use tabled::{
papergrid::{records::Records, width::WidthEstimator, Estimate, Offset::Begin},
Table, TableOption,
};
pub struct SetHorizontalChar {
c1: char,
c2: char,
line: usize,
position: usize,
}
impl SetHorizontalChar {
pub fn new(c1: char, c2: char, line: usize, position: usize) -> Self {
Self {
c1,
c2,
line,
position,
}
}
}
impl<R> TableOption<R> for SetHorizontalChar
where
R: Records,
{
fn change(&mut self, table: &mut Table<R>) {
let shape = table.shape();
let is_last_line = self.line == (shape.0 * 2);
let mut row = self.line;
if is_last_line {
row = self.line - 1;
}
let mut evaluator = WidthEstimator::default();
evaluator.estimate(table.get_records(), table.get_config());
let widths: Vec<_> = evaluator.into();
let mut i = 0;
#[allow(clippy::needless_range_loop)]
for column in 0..shape.1 {
let has_vertical = table.get_config().has_vertical(column, shape.1);
if has_vertical {
if self.position == i {
let mut border = table.get_config().get_border((row, column), shape);
if is_last_line {
border.left_bottom_corner = Some(self.c1);
} else {
border.left_top_corner = Some(self.c1);
}
table.get_config_mut().set_border((row, column), border);
return;
}
i += 1;
}
let width = widths[column];
if self.position < i + width {
let offset = self.position + 1 - i;
// let offset = width - offset;
table.get_config_mut().override_horizontal_border(
(self.line, column),
self.c2,
Begin(offset),
);
return;
}
i += width;
}
let has_vertical = table.get_config().has_vertical(shape.1, shape.1);
if self.position == i && has_vertical {
let mut border = table.get_config().get_border((row, shape.1), shape);
if is_last_line {
border.left_bottom_corner = Some(self.c1);
} else {
border.left_top_corner = Some(self.c1);
}
table.get_config_mut().set_border((row, shape.1), border);
}
}
}
}
mod width_increase {
use tabled::{
object::Cell,
papergrid::{
records::{Records, RecordsMut},
width::WidthEstimator,
Entity, Estimate, GridConfig,
},
peaker::PriorityNone,
Modify, Width,
};
use tabled::{peaker::Peaker, Table, TableOption};
#[derive(Debug)]
pub struct IncWidth(pub usize);
impl<R> TableOption<R> for IncWidth
where
R: Records + RecordsMut<String>,
{
fn change(&mut self, table: &mut Table<R>) {
if table.is_empty() {
return;
}
let (widths, total_width) =
get_table_widths_with_total(table.get_records(), table.get_config());
if total_width >= self.0 {
return;
}
let increase_list =
get_increase_list(widths, self.0, total_width, PriorityNone::default());
for (col, width) in increase_list.into_iter().enumerate() {
for row in 0..table.get_records().count_rows() {
let pad = table.get_config().get_padding(Entity::Cell(row, col));
let width = width - pad.left.size - pad.right.size;
table.with(Modify::new(Cell(row, col)).with(Width::increase(width)));
}
}
}
}
fn get_increase_list<F>(
mut widths: Vec<usize>,
total_width: usize,
mut width: usize,
mut peaker: F,
) -> Vec<usize>
where
F: Peaker,
{
while width != total_width {
let col = match peaker.peak(&[], &widths) {
Some(col) => col,
None => break,
};
widths[col] += 1;
width += 1;
}
widths
}
fn get_table_widths_with_total<R>(records: R, cfg: &GridConfig) -> (Vec<usize>, usize)
where
R: Records,
{
let mut evaluator = WidthEstimator::default();
evaluator.estimate(&records, cfg);
let total_width = get_table_total_width(&records, cfg, &evaluator);
let widths = evaluator.into();
(widths, total_width)
}
pub(crate) fn get_table_total_width<W, R>(records: R, cfg: &GridConfig, ctrl: &W) -> usize
where
W: Estimate<R>,
R: Records,
{
ctrl.total()
+ cfg.count_vertical(records.count_columns())
+ cfg.get_margin().left.size
+ cfg.get_margin().right.size
}
}

View File

@ -0,0 +1,193 @@
use nu_engine::CallExt;
use nu_protocol::ast::{Call, Expr, Expression};
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, DataSource, Example, IntoPipelineData, PipelineData, PipelineMetadata, ShellError,
Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
pub struct Metadata;
impl Command for Metadata {
fn name(&self) -> &str {
"metadata"
}
fn usage(&self) -> &str {
"Get the metadata for items in the stream"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("metadata")
.input_output_types(vec![(Type::Nothing, Type::Record(vec![]))])
.allow_variants_without_examples(true)
.optional(
"expression",
SyntaxShape::Any,
"the expression you want metadata for",
)
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let arg = call.positional_nth(0);
let head = call.head;
match arg {
Some(Expression {
expr: Expr::FullCellPath(full_cell_path),
span,
..
}) => {
if full_cell_path.tail.is_empty() {
match &full_cell_path.head {
Expression {
expr: Expr::Var(var_id),
..
} => {
let origin = stack.get_var_with_origin(*var_id, *span)?;
Ok(build_metadata_record(&origin, &input.metadata(), head)
.into_pipeline_data())
}
_ => {
let val: Value = call.req(engine_state, stack, 0)?;
Ok(build_metadata_record(&val, &input.metadata(), head)
.into_pipeline_data())
}
}
} else {
let val: Value = call.req(engine_state, stack, 0)?;
Ok(build_metadata_record(&val, &input.metadata(), head).into_pipeline_data())
}
}
Some(_) => {
let val: Value = call.req(engine_state, stack, 0)?;
Ok(build_metadata_record(&val, &input.metadata(), head).into_pipeline_data())
}
None => {
let mut cols = vec![];
let mut vals = vec![];
if let Some(x) = input.metadata().as_deref() {
match x {
PipelineMetadata {
data_source: DataSource::Ls,
} => {
cols.push("source".into());
vals.push(Value::string("ls", head))
}
PipelineMetadata {
data_source: DataSource::HtmlThemes,
} => {
cols.push("source".into());
vals.push(Value::string("into html --list", head))
}
PipelineMetadata {
data_source: DataSource::Profiling(values),
} => {
cols.push("profiling".into());
vals.push(Value::list(values.clone(), head))
}
}
}
Ok(Value::Record {
cols,
vals,
span: head,
}
.into_pipeline_data())
}
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get the metadata of a variable",
example: "let a = 42; metadata $a",
result: None,
},
Example {
description: "Get the metadata of the input",
example: "ls | metadata",
result: None,
},
]
}
}
fn build_metadata_record(
arg: &Value,
metadata: &Option<Box<PipelineMetadata>>,
head: Span,
) -> Value {
let mut cols = vec![];
let mut vals = vec![];
if let Ok(span) = arg.span() {
cols.push("span".into());
vals.push(Value::Record {
cols: vec!["start".into(), "end".into()],
vals: vec![
Value::Int {
val: span.start as i64,
span,
},
Value::Int {
val: span.end as i64,
span,
},
],
span: head,
});
}
if let Some(x) = metadata.as_deref() {
match x {
PipelineMetadata {
data_source: DataSource::Ls,
} => {
cols.push("source".into());
vals.push(Value::string("ls", head))
}
PipelineMetadata {
data_source: DataSource::HtmlThemes,
} => {
cols.push("source".into());
vals.push(Value::string("into html --list", head))
}
PipelineMetadata {
data_source: DataSource::Profiling(values),
} => {
cols.push("profiling".into());
vals.push(Value::list(values.clone(), head))
}
}
}
Value::Record {
cols,
vals,
span: head,
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(Metadata {})
}
}

View File

@ -0,0 +1,23 @@
mod debug_;
mod explain;
mod inspect;
mod inspect_table;
mod metadata;
mod profile;
mod timeit;
mod view;
mod view_files;
mod view_source;
mod view_span;
pub use debug_::Debug;
pub use explain::Explain;
pub use inspect::Inspect;
pub use inspect_table::build_table;
pub use metadata::Metadata;
pub use profile::Profile;
pub use timeit::TimeIt;
pub use view::View;
pub use view_files::ViewFiles;
pub use view_source::ViewSource;
pub use view_span::ViewSpan;

View File

@ -0,0 +1,113 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Closure, Command, EngineState, ProfilingConfig, Stack};
use nu_protocol::{
Category, DataSource, Example, IntoPipelineData, PipelineData, PipelineMetadata, Signature,
Spanned, SyntaxShape, Type, Value,
};
#[derive(Clone)]
pub struct Profile;
impl Command for Profile {
fn name(&self) -> &str {
"profile"
}
fn usage(&self) -> &str {
"Profile each pipeline element in a closure."
}
fn extra_usage(&self) -> &str {
r#"The command collects run time of every pipeline element, recursively stepping into child closures
until a maximum depth. Optionally, it also collects the source code and intermediate values.
Current known limitations are:
* profiling data from subexpressions is not tracked
* it does not step into loop iterations"#
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("profile")
.required(
"closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
"the closure to run",
)
.switch("source", "Collect source code in the report", None)
.switch("values", "Collect values in the report", None)
.named(
"max-depth",
SyntaxShape::Int,
"How many levels of blocks to step into (default: 1)",
Some('d'),
)
.input_output_types(vec![(Type::Any, Type::Table(vec![]))])
.allow_variants_without_examples(true)
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let capture_block: Spanned<Closure> = call.req(engine_state, stack, 0)?;
let block = engine_state.get_block(capture_block.item.block_id);
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let mut stack = stack.captures_to_stack(&capture_block.item.captures);
let input_val = input.into_value(call.head);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input_val.clone());
}
}
stack.profiling_config = ProfilingConfig::new(
call.get_flag::<i64>(engine_state, &mut stack, "max-depth")?
.unwrap_or(1),
call.has_flag("source"),
call.has_flag("values"),
);
let profiling_metadata = Box::new(PipelineMetadata {
data_source: DataSource::Profiling(vec![]),
});
let result = if let Some(PipelineMetadata {
data_source: DataSource::Profiling(values),
}) = eval_block(
engine_state,
&mut stack,
block,
input_val.into_pipeline_data_with_metadata(profiling_metadata),
redirect_stdout,
redirect_stderr,
)?
.metadata()
.map(|m| *m)
{
Value::list(values, call.head)
} else {
Value::nothing(call.head)
};
Ok(result.into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description:
"Profile some code, stepping into the `spam` command and collecting source.",
example: r#"def spam [] { "spam" }; profile { spam | str length } -d 2 --source"#,
result: None,
}]
}
}

View File

@ -0,0 +1,135 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::{
ast::Call,
engine::{Closure, Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type,
Value,
};
use std::time::Instant;
#[derive(Clone)]
pub struct TimeIt;
impl Command for TimeIt {
fn name(&self) -> &str {
"timeit"
}
fn usage(&self) -> &str {
"Time the running time of a closure"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("timeit")
.required(
"closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
"the closure to run",
)
.input_output_types(vec![
(Type::Any, Type::Duration),
(Type::Nothing, Type::Duration),
])
.allow_variants_without_examples(true)
.category(Category::Debug)
}
fn search_terms(&self) -> Vec<&str> {
vec!["timing", "timer", "benchmark", "measure"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let capture_block: Closure = call.req(engine_state, stack, 0)?;
let block = engine_state.get_block(capture_block.block_id);
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let mut stack = stack.captures_to_stack(&capture_block.captures);
// In order to provide the pipeline as a positional, it must be converted into a value.
// But because pipelines do not have Clone, this one has to be cloned as a value
// and then converted back into a pipeline for eval_block().
// So, the metadata must be saved here and restored at that point.
let input_metadata = input.metadata();
let input_val = input.into_value(call.head);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input_val.clone());
}
}
// Get the start time after all other computation has been done.
let start_time = Instant::now();
eval_block(
engine_state,
&mut stack,
block,
input_val.into_pipeline_data_with_metadata(input_metadata),
redirect_stdout,
redirect_stderr,
)?
.into_value(call.head);
let end_time = Instant::now();
let output = Value::Duration {
val: (end_time - start_time).as_nanos() as i64,
span: call.head,
};
Ok(output.into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Times a command within a closure",
example: "timeit { sleep 500ms }",
result: None,
},
Example {
description: "Times a command using an existing input",
example: "http get https://www.nushell.sh/book/ | timeit { split chars }",
result: None,
},
]
}
}
#[test]
// Due to difficulty in observing side-effects from time closures,
// checks that the closures have run correctly must use the filesystem.
fn test_time_closure() {
use nu_test_support::{nu, nu_repl_code, playground::Playground};
Playground::setup("test_time_closure", |dirs, _| {
let inp = [
r#"[2 3 4] | timeit { to nuon | save foo.txt }"#,
"open foo.txt",
];
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
assert_eq!(actual_repl.err, "");
assert_eq!(actual_repl.out, "[2, 3, 4]");
});
}
#[test]
fn test_time_closure_2() {
use nu_test_support::{nu, nu_repl_code, playground::Playground};
Playground::setup("test_time_closure", |dirs, _| {
let inp = [
r#"[2 3 4] | timeit {|e| {result: $e} | to nuon | save foo.txt }"#,
"open foo.txt",
];
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
assert_eq!(actual_repl.err, "");
assert_eq!(actual_repl.out, "{result: [2, 3, 4]}");
});
}

View File

@ -0,0 +1,49 @@
use nu_engine::get_full_help;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
#[derive(Clone)]
pub struct View;
impl Command for View {
fn name(&self) -> &str {
"view"
}
fn signature(&self) -> Signature {
Signature::build("view")
.category(Category::Debug)
.input_output_types(vec![(Type::Nothing, Type::String)])
}
fn usage(&self) -> &str {
"Various commands for viewing debug information"
}
fn extra_usage(&self) -> &str {
"You must use one of the following subcommands. Using this command as-is will only produce this help message."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(Value::String {
val: get_full_help(
&View.signature(),
&View.examples(),
engine_state,
stack,
self.is_parser_keyword(),
),
span: call.head,
}
.into_pipeline_data())
}
}

View File

@ -0,0 +1,70 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
#[derive(Clone)]
pub struct ViewFiles;
impl Command for ViewFiles {
fn name(&self) -> &str {
"view files"
}
fn usage(&self) -> &str {
"View the files registered in nushell's EngineState memory"
}
fn extra_usage(&self) -> &str {
"These are files parsed and loaded at runtime."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("view files")
.input_output_types(vec![(Type::Nothing, Type::String)])
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut records = vec![];
for (file, start, end) in engine_state.files() {
records.push(Value::Record {
cols: vec![
"filename".to_string(),
"start".to_string(),
"end".to_string(),
"size".to_string(),
],
vals: vec![
Value::string(file, call.head),
Value::int(*start as i64, call.head),
Value::int(*end as i64, call.head),
Value::int(*end as i64 - *start as i64, call.head),
],
span: call.head,
});
}
Ok(Value::List {
vals: records,
span: call.head,
}
.into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "View the files registered in nushell's EngineState memory",
example: r#"view files"#,
result: None,
}]
}
}

View File

@ -0,0 +1,194 @@
use itertools::Itertools;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type,
Value,
};
#[derive(Clone)]
pub struct ViewSource;
impl Command for ViewSource {
fn name(&self) -> &str {
"view source"
}
fn usage(&self) -> &str {
"View a block, module, or a definition"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("view source")
.input_output_types(vec![(Type::Nothing, Type::String)])
.required("item", SyntaxShape::Any, "name or block to view")
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let arg: Value = call.req(engine_state, stack, 0)?;
let arg_span = arg.span()?;
match arg {
Value::Block { val: block_id, .. } | Value::Closure { val: block_id, .. } => {
let block = engine_state.get_block(block_id);
if let Some(span) = block.span {
let contents = engine_state.get_span_contents(&span);
Ok(Value::string(String::from_utf8_lossy(contents), call.head)
.into_pipeline_data())
} else {
Ok(Value::string("<internal command>", call.head).into_pipeline_data())
}
}
Value::String { val, .. } => {
if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) {
// arg is a command
let decl = engine_state.get_decl(decl_id);
let sig = decl.signature();
let vec_of_required = &sig.required_positional;
let vec_of_optional = &sig.optional_positional;
let vec_of_flags = &sig.named;
// gets vector of positionals.
if let Some(block_id) = decl.get_block_id() {
let block = engine_state.get_block(block_id);
if let Some(block_span) = block.span {
let contents = engine_state.get_span_contents(&block_span);
let mut final_contents = String::from("def ");
final_contents.push_str(&val);
// The name of the function...
final_contents.push_str(" [ ");
for n in vec_of_required {
final_contents.push_str(&n.name);
// name of positional arg
final_contents.push(':');
final_contents.push_str(&n.shape.to_string());
final_contents.push(' ');
}
for n in vec_of_optional {
final_contents.push_str(&n.name);
// name of positional arg
final_contents.push_str("?:");
final_contents.push_str(&n.shape.to_string());
final_contents.push(' ');
}
for n in vec_of_flags {
final_contents.push_str("--");
final_contents.push_str(&n.long);
final_contents.push(' ');
if n.short.is_some() {
final_contents.push_str("(-");
final_contents.push(n.short.expect("this cannot trigger."));
final_contents.push(')');
}
if n.arg.is_some() {
final_contents.push_str(": ");
final_contents.push_str(
&n.arg.as_ref().expect("this cannot trigger.").to_string(),
);
}
final_contents.push(' ');
}
final_contents.push_str("] ");
final_contents.push_str(&String::from_utf8_lossy(contents));
Ok(Value::string(final_contents, call.head).into_pipeline_data())
} else {
Err(ShellError::GenericError(
"Cannot view value".to_string(),
"the command does not have a viewable block".to_string(),
Some(arg_span),
None,
Vec::new(),
))
}
} else {
Err(ShellError::GenericError(
"Cannot view value".to_string(),
"the command does not have a viewable block".to_string(),
Some(arg_span),
None,
Vec::new(),
))
}
} else if let Some(module_id) = engine_state.find_module(val.as_bytes(), &[]) {
// arg is a module
let module = engine_state.get_module(module_id);
if let Some(module_span) = module.span {
let contents = engine_state.get_span_contents(&module_span);
Ok(Value::string(String::from_utf8_lossy(contents), call.head)
.into_pipeline_data())
} else {
Err(ShellError::GenericError(
"Cannot view value".to_string(),
"the module does not have a viewable block".to_string(),
Some(arg_span),
None,
Vec::new(),
))
}
} else if let Some(alias_id) = engine_state.find_alias(val.as_bytes(), &[]) {
let contents = &mut engine_state.get_alias(alias_id).iter().map(|span| {
String::from_utf8_lossy(engine_state.get_span_contents(span)).to_string()
});
Ok(Value::String {
val: contents.join(" "),
span: call.head,
}
.into_pipeline_data())
} else {
Err(ShellError::GenericError(
"Cannot view value".to_string(),
"this name does not correspond to a viewable value".to_string(),
Some(arg_span),
None,
Vec::new(),
))
}
}
_ => Err(ShellError::GenericError(
"Cannot view value".to_string(),
"this value cannot be viewed".to_string(),
Some(arg_span),
None,
Vec::new(),
)),
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "View the source of a code block",
example: r#"let abc = { echo 'hi' }; view source $abc"#,
result: Some(Value::test_string("{ echo 'hi' }")),
},
Example {
description: "View the source of a custom command",
example: r#"def hi [] { echo 'Hi!' }; view source hi"#,
result: Some(Value::test_string("{ echo 'Hi!' }")),
},
Example {
description: "View the source of a custom command, which participates in the caller environment",
example: r#"def-env foo [] { let-env BAR = 'BAZ' }; view source foo"#,
result: Some(Value::test_string("{ let-env BAR = 'BAZ' }")),
},
Example {
description: "View the source of a module",
example: r#"module mod-foo { export-env { let-env FOO_ENV = 'BAZ' } }; view source mod-foo"#,
result: Some(Value::test_string(" export-env { let-env FOO_ENV = 'BAZ' }")),
},
Example {
description: "View the source of an alias",
example: r#"alias hello = echo hi; view source hello"#,
result: Some(Value::test_string("echo hi")),
},
]
}
}

View File

@ -0,0 +1,68 @@
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned,
SyntaxShape, Type, Value,
};
#[derive(Clone)]
pub struct ViewSpan;
impl Command for ViewSpan {
fn name(&self) -> &str {
"view span"
}
fn usage(&self) -> &str {
"View the contents of a span"
}
fn extra_usage(&self) -> &str {
"This command is meant for debugging purposes.\nIt allows you to view the contents of nushell spans.\nOne way to get spans is to pipe something into 'debug --raw'.\nThen you can use the Span { start, end } values as the start and end values for this command."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("view span")
.input_output_types(vec![(Type::Nothing, Type::String)])
.required("start", SyntaxShape::Int, "start of the span")
.required("end", SyntaxShape::Int, "end of the span")
.category(Category::Debug)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let start_span: Spanned<usize> = call.req(engine_state, stack, 0)?;
let end_span: Spanned<usize> = call.req(engine_state, stack, 1)?;
if start_span.item < end_span.item {
let bin_contents =
engine_state.get_span_contents(&Span::new(start_span.item, end_span.item));
Ok(
Value::string(String::from_utf8_lossy(bin_contents), call.head)
.into_pipeline_data(),
)
} else {
Err(ShellError::GenericError(
"Cannot view span".to_string(),
"this start and end does not correspond to a viewable value".to_string(),
Some(call.head),
None,
Vec::new(),
))
}
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "View the source of a span. 1 and 2 are just example values. Use the return of debug -r to get the actual values",
example: r#"some | pipeline | or | variable | debug -r; view span 1 2"#,
result: None,
}]
}
}