diff --git a/crates/nu-cmd-lang/src/core_commands/describe.rs b/crates/nu-cmd-lang/src/core_commands/describe.rs index 48b04cd9ae..a3803a5961 100644 --- a/crates/nu-cmd-lang/src/core_commands/describe.rs +++ b/crates/nu-cmd-lang/src/core_commands/describe.rs @@ -1,7 +1,8 @@ use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::{ - Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value, + record, Category, Example, IntoPipelineData, PipelineData, PipelineMetadata, Record, + ShellError, Signature, Type, Value, }; #[derive(Clone)] @@ -18,12 +19,21 @@ impl Command for Describe { fn signature(&self) -> Signature { Signature::build("describe") - .input_output_types(vec![(Type::Any, Type::String)]) + .input_output_types(vec![ + (Type::Any, Type::String), + (Type::Any, Type::Record(vec![])), + ]) .switch( "no-collect", "do not collect streams of structured data", Some('n'), ) + .switch( + "detailed", + "show detailed information about the value", + Some('d'), + ) + .switch("collect-lazyrecords", "collect lazy records", Some('l')) .category(Category::Core) } @@ -33,12 +43,12 @@ impl Command for Describe { fn run( &self, - _engine_state: &EngineState, + engine_state: &EngineState, _stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { - run(call, input) + run(Some(engine_state), call, input) } fn run_const( @@ -47,7 +57,7 @@ impl Command for Describe { call: &Call, input: PipelineData, ) -> Result { - run(call, input) + run(None, call, input) } fn examples(&self) -> Vec { @@ -57,7 +67,60 @@ impl Command for Describe { example: "'hello' | describe", result: Some(Value::test_string("string")), }, + Example { + description: "Describe the type of a record in a detailed way", + example: + "{shell:'true', uwu:true, features: {bugs:false, multiplatform:true, speed: 10}, fib: [1 1 2 3 5 8], on_save: {|x| print $'Saving ($x)'}, first_commit: 2019-05-10, my_duration: (4min + 20sec)} | describe -d", + result: Some(Value::test_record(record!( + "type" => Value::test_string("record"), + "lazy" => Value::test_bool(false), + "columns" => Value::test_record(record!( + "shell" => Value::test_string("string"), + "uwu" => Value::test_string("bool"), + "features" => Value::test_record(record!( + "type" => Value::test_string("record"), + "lazy" => Value::test_bool(false), + "columns" => Value::test_record(record!( + "bugs" => Value::test_string("bool"), + "multiplatform" => Value::test_string("bool"), + "speed" => Value::test_string("int"), + )), + )), + "fib" => Value::test_record(record!( + "type" => Value::test_string("list"), + "length" => Value::test_int(6), + "values" => Value::test_list(vec![ + Value::test_string("int"), + Value::test_string("int"), + Value::test_string("int"), + Value::test_string("int"), + Value::test_string("int"), + Value::test_string("int"), + ]), + )), + "on_save" => Value::test_record(record!( + "type" => Value::test_string("closure"), + "signature" => Value::test_record(record!( + "name" => Value::test_string(""), + "category" => Value::test_string("default"), + )), + )), + "first_commit" => Value::test_string("date"), + "my_duration" => Value::test_string("duration"), + )), + ))), + }, + // TODO: Uncomment these tests when/if we allow external commands in examples. /* + Example { + description: "Describe the type of a stream with detailed information", + example: "[1 2 3] | each {|i| $i} | describe -d", + result: Some(Value::test_record(record!( + "type" => Value::test_string("stream"), + "origin" => Value::test_string("nushell"), + "subtype" => Value::test_string("int"), + ))), + }, Example { description: "Describe a stream of data, collecting it first", example: "[1 2 3] | each {|i| $i} | describe", @@ -77,16 +140,90 @@ impl Command for Describe { } } -fn run(call: &Call, input: PipelineData) -> Result { +fn run( + engine_state: Option<&EngineState>, + call: &Call, + input: PipelineData, +) -> Result { + let metadata = input.metadata().clone(); + let head = call.head; let no_collect: bool = call.has_flag("no-collect"); + let detailed = call.has_flag("detailed"); - let description = match input { - PipelineData::ExternalStream { .. } => "raw input".into(), + let description: Value = match input { + PipelineData::ExternalStream { + ref stdout, + ref stderr, + ref exit_code, + .. + } => { + if detailed { + Value::record( + record!( + "type" => Value::string("stream", head), + "origin" => Value::string("external", head), + "stdout" => match stdout { + Some(_) => Value::record( + record!( + "type" => Value::string("stream", head), + "origin" => Value::string("external", head), + "subtype" => Value::string("any", head), + ), + head, + ), + None => Value::nothing(head), + }, + "stderr" => match stderr { + Some(_) => Value::record( + record!( + "type" => Value::string("stream", head), + "origin" => Value::string("external", head), + "subtype" => Value::string("any", head), + ), + head, + ), + None => Value::nothing(head), + }, + "exit_code" => match exit_code { + Some(_) => Value::record( + record!( + "type" => Value::string("stream", head), + "origin" => Value::string("external", head), + "subtype" => Value::string("int", head), + ), + head, + ), + None => Value::nothing(head), + }, + "metadata" => metadata_to_value(metadata, head), + ), + head, + ) + } else { + Value::string("raw input", head) + } + } PipelineData::ListStream(_, _) => { - if no_collect { - "stream".into() + if detailed { + Value::record( + record!( + "type" => Value::string("stream", head), + "origin" => Value::string("nushell", head), + "subtype" => { + if no_collect { + Value::string("any", head) + } else { + describe_value(input.into_value(head), head, engine_state, call)? + } + }, + "metadata" => metadata_to_value(metadata, head), + ), + head, + ) + } else if no_collect { + Value::string("stream", head) } else { let value = input.into_value(head); let base_description = match value { @@ -94,19 +231,190 @@ fn run(call: &Call, input: PipelineData) -> Result { _ => value.get_type().to_string(), }; - format!("{base_description} (stream)") + Value::string(format!("{} (stream)", base_description), head) } } _ => { let value = input.into_value(head); - match value { - Value::CustomValue { val, .. } => val.value_string(), - _ => value.get_type().to_string(), + if !detailed { + match value { + Value::CustomValue { val, .. } => Value::string(val.value_string(), head), + _ => Value::string(value.get_type().to_string(), head), + } + } else { + describe_value(value, head, engine_state, call)? } } }; - Ok(Value::string(description, head).into_pipeline_data()) + Ok(description.into_pipeline_data()) +} + +fn describe_value( + value: Value, + head: nu_protocol::Span, + engine_state: Option<&EngineState>, + call: &Call, +) -> Result { + Ok(match value { + Value::CustomValue { val, internal_span } => Value::record( + record!( + "type" => Value::string("custom", head), + "subtype" => run(engine_state,call, val.to_base_value(internal_span)?.into_pipeline_data())?.into_value(head), + ), + head, + ), + Value::Bool { .. } + | Value::Int { .. } + | Value::Float { .. } + | Value::Filesize { .. } + | Value::Duration { .. } + | Value::Date { .. } + | Value::Range { .. } + | Value::String { .. } + | Value::MatchPattern { .. } + | Value::Nothing { .. } => Value::record( + record!( + "type" => Value::string(value.get_type().to_string(), head), + ), + head, + ), + Value::Record { val, .. } => { + let mut record = Record::new(); + for i in 0..val.len() { + let k = val.cols[i].clone(); + let v = val.vals[i].clone(); + + record.push(k, { + if let Value::Record { val, .. } = + describe_value(v.clone(), head, engine_state, call)? + { + if let [Value::String { val: k, .. }] = val.vals.as_slice() { + Value::string(k, head) + } else { + Value::record(val, head) + } + } else { + describe_value(v, head, engine_state, call)? + } + }); + } + + Value::record( + record!( + "type" => Value::string("record", head), + "lazy" => Value::bool(false, head), + "columns" => Value::record(record, head), + ), + head, + ) + } + Value::List { vals, .. } => Value::record( + record!( + "type" => Value::string("list", head), + "length" => Value::int(vals.len() as i64, head), + "values" => Value::list(vals.iter().map(|v| + match describe_value(v.clone(), head, engine_state, call) { + Ok(Value::Record {val, ..}) => if val.cols.as_slice() == ["type"] {Ok(val.vals[0].clone())} else {Ok(Value::record(val, head))}, + x => x + } + ).collect::, _>>()?, head), + ), + head, + ), + Value::Block { val, .. } | Value::Closure { val, .. } => { + let block = engine_state.map(|engine_state| engine_state.get_block(val)); + + if let Some(block) = block { + let mut record = Record::new(); + record.push("type", Value::string(value.get_type().to_string(), head)); + record.push( + "signature", + Value::record( + record!( + "name" => Value::string(block.signature.name.clone(), head), + "category" => Value::string(block.signature.category.to_string(), head), + ), + head, + ), + ); + Value::record(record, head) + } else { + Value::record( + record!( + "type" => Value::string("closure", head), + ), + head, + ) + } + } + + Value::Error { error, .. } => Value::record( + record!( + "type" => Value::string("error", head), + "subtype" => Value::string(error.to_string(), head), + ), + head, + ), + Value::Binary { val, .. } => Value::record( + record!( + "type" => Value::string("binary", head), + "length" => Value::int(val.len() as i64, head), + ), + head, + ), + Value::CellPath { val, .. } => Value::record( + record!( + "type" => Value::string("cellpath", head), + "length" => Value::int(val.members.len() as i64, head), + ), + head, + ), + Value::LazyRecord { val, .. } => { + let collect_lazyrecords: bool = call.has_flag("collect-lazyrecords"); + let mut record = Record::new(); + + record.push("type", Value::string("record", head)); + record.push("lazy", Value::bool(true, head)); + + if collect_lazyrecords { + let collected = val.collect()?; + if let Value::Record { val, .. } = + describe_value(collected, head, engine_state, call)? + { + let mut record_cols = Record::new(); + record.push("length", Value::int(val.len() as i64, head)); + + for i in 0..val.len() { + record_cols.push( + val.cols[i].clone(), + describe_value(val.vals[i].clone(), head, engine_state, call)?, + ); + } + } else { + let cols = val.column_names(); + record.push("length", Value::int(cols.len() as i64, head)); + } + } else { + let cols = val.column_names(); + record.push("length", Value::int(cols.len() as i64, head)); + } + + Value::record(record!(), head) + } + }) +} + +fn metadata_to_value(metadata: Option>, head: nu_protocol::Span) -> Value { + match metadata { + Some(metadata) => Value::record( + record!( + "data_source" => Value::string(format!("{:?}", metadata.data_source), head), + ), + head, + ), + _ => Value::nothing(head), + } } #[cfg(test)] diff --git a/crates/nu-cmd-lang/src/example_support.rs b/crates/nu-cmd-lang/src/example_support.rs index db34962f80..44efa911da 100644 --- a/crates/nu-cmd-lang/src/example_support.rs +++ b/crates/nu-cmd-lang/src/example_support.rs @@ -1,6 +1,6 @@ use itertools::Itertools; use nu_protocol::{ - ast::Block, + ast::{Block, RangeInclusion}, engine::{EngineState, Stack, StateDelta, StateWorkingSet}, Example, PipelineData, Signature, Span, Type, Value, }; @@ -143,7 +143,8 @@ pub fn check_example_evaluates_to_expected_output( // you need to define its equality in the Value struct if let Some(expected) = example.result.as_ref() { assert_eq!( - &result, expected, + DebuggableValue(&result), + DebuggableValue(expected), "The example result differs from the expected value", ) } @@ -184,3 +185,103 @@ fn eval( let (block, delta) = parse(contents, engine_state); eval_block(block, input, cwd, engine_state, delta) } + +pub struct DebuggableValue<'a>(pub &'a Value); + +impl PartialEq for DebuggableValue<'_> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl<'a> std::fmt::Debug for DebuggableValue<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Value::Bool { val, .. } => { + write!(f, "{:?}", val) + } + Value::Int { val, .. } => { + write!(f, "{:?}", val) + } + Value::Float { val, .. } => { + write!(f, "{:?}f", val) + } + Value::Filesize { val, .. } => { + write!(f, "Filesize({:?})", val) + } + Value::Duration { val, .. } => { + let duration = std::time::Duration::from_nanos(*val as u64); + write!(f, "Duration({:?})", duration) + } + Value::Date { val, .. } => { + write!(f, "Date({:?})", val) + } + Value::Range { val, .. } => match val.inclusion { + RangeInclusion::Inclusive => write!( + f, + "Range({:?}..{:?}, step: {:?})", + val.from, val.to, val.incr + ), + RangeInclusion::RightExclusive => write!( + f, + "Range({:?}..<{:?}, step: {:?})", + val.from, val.to, val.incr + ), + }, + Value::String { val, .. } => { + write!(f, "{:?}", val) + } + Value::Record { val, .. } => { + write!(f, "{{")?; + for i in 0..val.len() { + let col = &val.cols[i]; + let value = &val.vals[i]; + + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{:?}: {:?}", col, DebuggableValue(value))?; + } + write!(f, "}}") + } + Value::List { vals, .. } => { + write!(f, "[")?; + for (i, value) in vals.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{:?}", DebuggableValue(value))?; + } + write!(f, "]") + } + Value::Block { val, .. } => { + write!(f, "Block({:?})", val) + } + Value::Closure { val, .. } => { + write!(f, "Closure({:?})", val) + } + Value::Nothing { .. } => { + write!(f, "Nothing") + } + Value::Error { error, .. } => { + write!(f, "Error({:?})", error) + } + Value::Binary { val, .. } => { + write!(f, "Binary({:?})", val) + } + Value::CellPath { val, .. } => { + write!(f, "CellPath({:?})", val.into_string()) + } + Value::CustomValue { val, .. } => { + write!(f, "CustomValue({:?})", val) + } + Value::LazyRecord { val, .. } => { + let rec = val.collect().map_err(|_| std::fmt::Error)?; + write!(f, "LazyRecord({:?})", DebuggableValue(&rec)) + } + Value::MatchPattern { val, .. } => { + write!(f, "MatchPattern({:?})", val) + } + } + } +}