mirror of
https://github.com/nushell/nushell
synced 2024-12-26 13:03:07 +00:00
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:
parent
d3182a6737
commit
c799f77577
2 changed files with 426 additions and 17 deletions
|
@ -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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
run(call, input)
|
||||
run(None, call, input)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
|
@ -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<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 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<PipelineData, ShellError> {
|
|||
_ => 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<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)]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue