Add detailed flag for describe (#10795)

- Add `detailed` flag for `describe`

- Improve detailed describe and better format when running examples.

# Rationale

For now, neither `describe` nor any of the `debug` commands provide an
easy and structured way of inspecting the data's type and more. This
flag provides a structured way of getting such information. Allows also
to avoid the rather hacky solution
```nu
$in | describe | str replace --regex '<.*' ''
```

# User-facing changes

Adds a new flag to ``describe`.
This commit is contained in:
Gaëtan 2023-10-23 16:12:11 +02:00 committed by GitHub
parent d3182a6737
commit c799f77577
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 426 additions and 17 deletions

View file

@ -1,7 +1,8 @@
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet}; use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{ use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value, record, Category, Example, IntoPipelineData, PipelineData, PipelineMetadata, Record,
ShellError, Signature, Type, Value,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -18,12 +19,21 @@ impl Command for Describe {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("describe") 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( .switch(
"no-collect", "no-collect",
"do not collect streams of structured data", "do not collect streams of structured data",
Some('n'), Some('n'),
) )
.switch(
"detailed",
"show detailed information about the value",
Some('d'),
)
.switch("collect-lazyrecords", "collect lazy records", Some('l'))
.category(Category::Core) .category(Category::Core)
} }
@ -33,12 +43,12 @@ impl Command for Describe {
fn run( fn run(
&self, &self,
_engine_state: &EngineState, engine_state: &EngineState,
_stack: &mut Stack, _stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
run(call, input) run(Some(engine_state), call, input)
} }
fn run_const( fn run_const(
@ -47,7 +57,7 @@ impl Command for Describe {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
run(call, input) run(None, call, input)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -57,7 +67,60 @@ impl Command for Describe {
example: "'hello' | describe", example: "'hello' | describe",
result: Some(Value::test_string("string")), 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 { Example {
description: "Describe a stream of data, collecting it first", description: "Describe a stream of data, collecting it first",
example: "[1 2 3] | each {|i| $i} | describe", example: "[1 2 3] | each {|i| $i} | describe",
@ -77,16 +140,90 @@ impl Command for Describe {
} }
} }
fn run(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> { fn run(
engine_state: Option<&EngineState>,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let metadata = input.metadata().clone();
let head = call.head; let head = call.head;
let no_collect: bool = call.has_flag("no-collect"); let no_collect: bool = call.has_flag("no-collect");
let detailed = call.has_flag("detailed");
let description = match input { let description: Value = match input {
PipelineData::ExternalStream { .. } => "raw input".into(), 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(_, _) => { PipelineData::ListStream(_, _) => {
if no_collect { if detailed {
"stream".into() 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 { } else {
let value = input.into_value(head); let value = input.into_value(head);
let base_description = match value { let base_description = match value {
@ -94,19 +231,190 @@ fn run(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> {
_ => value.get_type().to_string(), _ => value.get_type().to_string(),
}; };
format!("{base_description} (stream)") Value::string(format!("{} (stream)", base_description), head)
} }
} }
_ => { _ => {
let value = input.into_value(head); let value = input.into_value(head);
match value { if !detailed {
Value::CustomValue { val, .. } => val.value_string(), match value {
_ => value.get_type().to_string(), 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<Value, ShellError> {
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::<Result<Vec<_>, _>>()?, 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<Box<PipelineMetadata>>, 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)] #[cfg(test)]

View file

@ -1,6 +1,6 @@
use itertools::Itertools; use itertools::Itertools;
use nu_protocol::{ use nu_protocol::{
ast::Block, ast::{Block, RangeInclusion},
engine::{EngineState, Stack, StateDelta, StateWorkingSet}, engine::{EngineState, Stack, StateDelta, StateWorkingSet},
Example, PipelineData, Signature, Span, Type, Value, 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 // you need to define its equality in the Value struct
if let Some(expected) = example.result.as_ref() { if let Some(expected) = example.result.as_ref() {
assert_eq!( assert_eq!(
&result, expected, DebuggableValue(&result),
DebuggableValue(expected),
"The example result differs from the expected value", "The example result differs from the expected value",
) )
} }
@ -184,3 +185,103 @@ fn eval(
let (block, delta) = parse(contents, engine_state); let (block, delta) = parse(contents, engine_state);
eval_block(block, input, cwd, engine_state, delta) 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)
}
}
}
}