1use nu_engine::command_prelude::*;
2use nu_protocol::{
3 BlockId, ByteStreamSource, Category, PipelineMetadata, Signature,
4 engine::{Closure, StateWorkingSet},
5};
6use std::any::type_name;
7#[derive(Clone)]
8pub struct Describe;
9
10impl Command for Describe {
11 fn name(&self) -> &str {
12 "describe"
13 }
14
15 fn description(&self) -> &str {
16 "Describe the type and structure of the value(s) piped in."
17 }
18
19 fn signature(&self) -> Signature {
20 Signature::build("describe")
21 .input_output_types(vec![(Type::Any, Type::Any)])
22 .switch(
23 "no-collect",
24 "do not collect streams of structured data",
25 Some('n'),
26 )
27 .switch(
28 "detailed",
29 "show detailed information about the value",
30 Some('d'),
31 )
32 .category(Category::Core)
33 }
34
35 fn is_const(&self) -> bool {
36 true
37 }
38
39 fn run(
40 &self,
41 engine_state: &EngineState,
42 stack: &mut Stack,
43 call: &Call,
44 input: PipelineData,
45 ) -> Result<PipelineData, ShellError> {
46 let options = Options {
47 no_collect: call.has_flag(engine_state, stack, "no-collect")?,
48 detailed: call.has_flag(engine_state, stack, "detailed")?,
49 };
50 run(Some(engine_state), call, input, options)
51 }
52
53 fn run_const(
54 &self,
55 working_set: &StateWorkingSet,
56 call: &Call,
57 input: PipelineData,
58 ) -> Result<PipelineData, ShellError> {
59 let options = Options {
60 no_collect: call.has_flag_const(working_set, "no-collect")?,
61 detailed: call.has_flag_const(working_set, "detailed")?,
62 };
63 run(None, call, input, options)
64 }
65
66 fn examples(&self) -> Vec<Example<'_>> {
67 vec![
68 Example {
69 description: "Describe the type of a string",
70 example: "'hello' | describe",
71 result: Some(Value::test_string("string")),
72 },
73 Example {
74 description: "Describe the type of a record in a detailed way",
75 example: "{shell:'true', uwu:true, features: {bugs:false, multiplatform:true, speed: 10}, fib: [1 1 2 3 5 8], on_save: {|x| $'Saving ($x)'}, first_commit: 2019-05-10, my_duration: (4min + 20sec)} | describe -d",
76 result: Some(Value::test_record(record!(
77 "type" => Value::test_string("record"),
78 "detailed_type" => Value::test_string("record<shell: string, uwu: bool, features: record<bugs: bool, multiplatform: bool, speed: int>, fib: list<int>, on_save: closure, first_commit: datetime, my_duration: duration>"),
79 "columns" => Value::test_record(record!(
80 "shell" => Value::test_record(record!(
81 "type" => Value::test_string("string"),
82 "detailed_type" => Value::test_string("string"),
83 "rust_type" => Value::test_string("&alloc::string::String"),
84 "value" => Value::test_string("true"),
85 )),
86 "uwu" => Value::test_record(record!(
87 "type" => Value::test_string("bool"),
88 "detailed_type" => Value::test_string("bool"),
89 "rust_type" => Value::test_string("bool"),
90 "value" => Value::test_bool(true),
91 )),
92 "features" => Value::test_record(record!(
93 "type" => Value::test_string("record"),
94 "detailed_type" => Value::test_string("record<bugs: bool, multiplatform: bool, speed: int>"),
95 "columns" => Value::test_record(record!(
96 "bugs" => Value::test_record(record!(
97 "type" => Value::test_string("bool"),
98 "detailed_type" => Value::test_string("bool"),
99 "rust_type" => Value::test_string("bool"),
100 "value" => Value::test_bool(false),
101 )),
102 "multiplatform" => Value::test_record(record!(
103 "type" => Value::test_string("bool"),
104 "detailed_type" => Value::test_string("bool"),
105 "rust_type" => Value::test_string("bool"),
106 "value" => Value::test_bool(true),
107 )),
108 "speed" => Value::test_record(record!(
109 "type" => Value::test_string("int"),
110 "detailed_type" => Value::test_string("int"),
111 "rust_type" => Value::test_string("i64"),
112 "value" => Value::test_int(10),
113 )),
114 )),
115 "rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
116 )),
117 "fib" => Value::test_record(record!(
118 "type" => Value::test_string("list"),
119 "detailed_type" => Value::test_string("list<int>"),
120 "length" => Value::test_int(6),
121 "rust_type" => Value::test_string("&mut alloc::vec::Vec<nu_protocol::value::Value>"),
122 "value" => Value::test_list(vec![
123 Value::test_record(record!(
124 "type" => Value::test_string("int"),
125 "detailed_type" => Value::test_string("int"),
126 "rust_type" => Value::test_string("i64"),
127 "value" => Value::test_int(1),
128 )),
129 Value::test_record(record!(
130 "type" => Value::test_string("int"),
131 "detailed_type" => Value::test_string("int"),
132 "rust_type" => Value::test_string("i64"),
133 "value" => Value::test_int(1),
134 )),
135 Value::test_record(record!(
136 "type" => Value::test_string("int"),
137 "detailed_type" => Value::test_string("int"),
138 "rust_type" => Value::test_string("i64"),
139 "value" => Value::test_int(2),
140 )),
141 Value::test_record(record!(
142 "type" => Value::test_string("int"),
143 "detailed_type" => Value::test_string("int"),
144 "rust_type" => Value::test_string("i64"),
145 "value" => Value::test_int(3),
146 )),
147 Value::test_record(record!(
148 "type" => Value::test_string("int"),
149 "detailed_type" => Value::test_string("int"),
150 "rust_type" => Value::test_string("i64"),
151 "value" => Value::test_int(5),
152 )),
153 Value::test_record(record!(
154 "type" => Value::test_string("int"),
155 "detailed_type" => Value::test_string("int"),
156 "rust_type" => Value::test_string("i64"),
157 "value" => Value::test_int(8),
158 ))]
159 ),
160 )),
161 "on_save" => Value::test_record(record!(
162 "type" => Value::test_string("closure"),
163 "detailed_type" => Value::test_string("closure"),
164 "rust_type" => Value::test_string("&alloc::boxed::Box<nu_protocol::engine::closure::Closure>"),
165 "value" => Value::test_closure(Closure {
166 block_id: BlockId::new(1),
167 captures: vec![],
168 }),
169 "signature" => Value::test_record(record!(
170 "name" => Value::test_string(""),
171 "category" => Value::test_string("default"),
172 )),
173 )),
174 "first_commit" => Value::test_record(record!(
175 "type" => Value::test_string("datetime"),
176 "detailed_type" => Value::test_string("datetime"),
177 "rust_type" => Value::test_string("chrono::datetime::DateTime<chrono::offset::fixed::FixedOffset>"),
178 "value" => Value::test_date("2019-05-10 00:00:00Z".parse().unwrap_or_default()),
179 )),
180 "my_duration" => Value::test_record(record!(
181 "type" => Value::test_string("duration"),
182 "detailed_type" => Value::test_string("duration"),
183 "rust_type" => Value::test_string("i64"),
184 "value" => Value::test_duration(260_000_000_000),
185 ))
186 )),
187 "rust_type" => Value::test_string("&nu_utils::shared_cow::SharedCow<nu_protocol::value::record::Record>"),
188 ))),
189 },
190 Example {
191 description: "Describe the type of a stream with detailed information",
192 example: "[1 2 3] | each {|i| echo $i} | describe -d",
193 result: None, },
208 Example {
209 description: "Describe a stream of data, collecting it first",
210 example: "[1 2 3] | each {|i| echo $i} | describe",
211 result: None, },
214 Example {
215 description: "Describe the input but do not collect streams",
216 example: "[1 2 3] | each {|i| echo $i} | describe --no-collect",
217 result: None, },
220 ]
221 }
222
223 fn search_terms(&self) -> Vec<&str> {
224 vec!["type", "typeof", "info", "structure"]
225 }
226}
227
228#[derive(Clone, Copy)]
229struct Options {
230 no_collect: bool,
231 detailed: bool,
232}
233
234fn run(
235 engine_state: Option<&EngineState>,
236 call: &Call,
237 input: PipelineData,
238 options: Options,
239) -> Result<PipelineData, ShellError> {
240 let head = call.head;
241 let metadata = input.metadata();
242
243 let description = match input {
244 PipelineData::ByteStream(stream, ..) => {
245 let type_ = stream.type_().describe();
246
247 let description = if options.detailed {
248 let origin = match stream.source() {
249 ByteStreamSource::Read(_) => "unknown",
250 ByteStreamSource::File(_) => "file",
251 #[cfg(feature = "os")]
252 ByteStreamSource::Child(_) => "external",
253 };
254
255 Value::record(
256 record! {
257 "type" => Value::string("bytestream", head),
258 "detailed_type" => Value::string(type_, head),
259 "rust_type" => Value::string(type_of(&stream), head),
260 "origin" => Value::string(origin, head),
261 "metadata" => metadata_to_value(metadata, head),
262 },
263 head,
264 )
265 } else {
266 Value::string(type_, head)
267 };
268
269 if !options.no_collect {
270 stream.drain()?;
271 }
272
273 description
274 }
275 PipelineData::ListStream(stream, ..) => {
276 let type_ = type_of(&stream);
277 if options.detailed {
278 let subtype = if options.no_collect {
279 Value::string("any", head)
280 } else {
281 describe_value(stream.into_debug_value(), head, engine_state)
282 };
283 Value::record(
284 record! {
285 "type" => Value::string("stream", head),
286 "detailed_type" => Value::string("list stream", head),
287 "rust_type" => Value::string(type_, head),
288 "origin" => Value::string("nushell", head),
289 "subtype" => subtype,
290 "metadata" => metadata_to_value(metadata, head),
291 },
292 head,
293 )
294 } else if options.no_collect {
295 Value::string("stream", head)
296 } else {
297 let value = stream.into_debug_value();
298 let base_description = value.get_type().to_string();
299 Value::string(format!("{base_description} (stream)"), head)
300 }
301 }
302 PipelineData::Value(value, ..) => {
303 if !options.detailed {
304 Value::string(value.get_type().to_string(), head)
305 } else {
306 describe_value(value, head, engine_state)
307 }
308 }
309 PipelineData::Empty => Value::string(Type::Nothing.to_string(), head),
310 };
311
312 Ok(description.into_pipeline_data())
313}
314
315enum Description {
316 Record(Record),
317}
318
319impl Description {
320 fn into_value(self, span: Span) -> Value {
321 match self {
322 Description::Record(record) => Value::record(record, span),
323 }
324 }
325}
326
327fn describe_value(value: Value, head: Span, engine_state: Option<&EngineState>) -> Value {
328 let Description::Record(record) = describe_value_inner(value, head, engine_state);
329 Value::record(record, head)
330}
331
332fn type_of<T>(_: &T) -> String {
333 type_name::<T>().to_string()
334}
335
336fn describe_value_inner(
337 mut value: Value,
338 head: Span,
339 engine_state: Option<&EngineState>,
340) -> Description {
341 let value_type = value.get_type().to_string();
342 match value {
343 Value::Bool { val, .. } => Description::Record(record! {
344 "type" => Value::string("bool", head),
345 "detailed_type" => Value::string(value_type, head),
346 "rust_type" => Value::string(type_of(&val), head),
347 "value" => value,
348 }),
349 Value::Int { val, .. } => Description::Record(record! {
350 "type" => Value::string("int", head),
351 "detailed_type" => Value::string(value_type, head),
352 "rust_type" => Value::string(type_of(&val), head),
353 "value" => value,
354 }),
355 Value::Float { val, .. } => Description::Record(record! {
356 "type" => Value::string("float", head),
357 "detailed_type" => Value::string(value_type, head),
358 "rust_type" => Value::string(type_of(&val), head),
359 "value" => value,
360 }),
361 Value::Filesize { val, .. } => Description::Record(record! {
362 "type" => Value::string("filesize", head),
363 "detailed_type" => Value::string(value_type, head),
364 "rust_type" => Value::string(type_of(&val), head),
365 "value" => value,
366 }),
367 Value::Duration { val, .. } => Description::Record(record! {
368 "type" => Value::string("duration", head),
369 "detailed_type" => Value::string(value_type, head),
370 "rust_type" => Value::string(type_of(&val), head),
371 "value" => value,
372 }),
373 Value::Date { val, .. } => Description::Record(record! {
374 "type" => Value::string("datetime", head),
375 "detailed_type" => Value::string(value_type, head),
376 "rust_type" => Value::string(type_of(&val), head),
377 "value" => value,
378 }),
379 Value::Range { ref val, .. } => Description::Record(record! {
380 "type" => Value::string("range", head),
381 "detailed_type" => Value::string(value_type, head),
382 "rust_type" => Value::string(type_of(&val), head),
383 "value" => value,
384 }),
385 Value::String { ref val, .. } => Description::Record(record! {
386 "type" => Value::string("string", head),
387 "detailed_type" => Value::string(value_type, head),
388 "rust_type" => Value::string(type_of(&val), head),
389 "value" => value,
390 }),
391 Value::Glob { ref val, .. } => Description::Record(record! {
392 "type" => Value::string("glob", head),
393 "detailed_type" => Value::string(value_type, head),
394 "rust_type" => Value::string(type_of(&val), head),
395 "value" => value,
396 }),
397 Value::Nothing { .. } => Description::Record(record! {
398 "type" => Value::string("nothing", head),
399 "detailed_type" => Value::string(value_type, head),
400 "rust_type" => Value::string("", head),
401 "value" => value,
402 }),
403 Value::Record { ref val, .. } => {
404 let mut columns = val.clone().into_owned();
405 for (_, val) in &mut columns {
406 *val =
407 describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
408 }
409
410 Description::Record(record! {
411 "type" => Value::string("record", head),
412 "detailed_type" => Value::string(value_type, head),
413 "columns" => Value::record(columns.clone(), head),
414 "rust_type" => Value::string(type_of(&val), head),
415 })
416 }
417 Value::List { ref mut vals, .. } => {
418 for val in &mut *vals {
419 *val =
420 describe_value_inner(std::mem::take(val), head, engine_state).into_value(head);
421 }
422
423 Description::Record(record! {
424 "type" => Value::string("list", head),
425 "detailed_type" => Value::string(value_type, head),
426 "length" => Value::int(vals.len() as i64, head),
427 "rust_type" => Value::string(type_of(&vals), head),
428 "value" => value,
429 })
430 }
431 Value::Closure { ref val, .. } => {
432 let block = engine_state.map(|engine_state| engine_state.get_block(val.block_id));
433
434 let mut record = record! {
435 "type" => Value::string("closure", head),
436 "detailed_type" => Value::string(value_type, head),
437 "rust_type" => Value::string(type_of(&val), head),
438 "value" => value,
439 };
440 if let Some(block) = block {
441 record.push(
442 "signature",
443 Value::record(
444 record! {
445 "name" => Value::string(block.signature.name.clone(), head),
446 "category" => Value::string(block.signature.category.to_string(), head),
447 },
448 head,
449 ),
450 );
451 }
452 Description::Record(record)
453 }
454 Value::Error { ref error, .. } => Description::Record(record! {
455 "type" => Value::string("error", head),
456 "detailed_type" => Value::string(value_type, head),
457 "subtype" => Value::string(error.to_string(), head),
458 "rust_type" => Value::string(type_of(&error), head),
459 "value" => value,
460 }),
461 Value::Binary { ref val, .. } => Description::Record(record! {
462 "type" => Value::string("binary", head),
463 "detailed_type" => Value::string(value_type, head),
464 "length" => Value::int(val.len() as i64, head),
465 "rust_type" => Value::string(type_of(&val), head),
466 "value" => value,
467 }),
468 Value::CellPath { ref val, .. } => Description::Record(record! {
469 "type" => Value::string("cell-path", head),
470 "detailed_type" => Value::string(value_type, head),
471 "length" => Value::int(val.members.len() as i64, head),
472 "rust_type" => Value::string(type_of(&val), head),
473 "value" => value
474 }),
475 Value::Custom { ref val, .. } => Description::Record(record! {
476 "type" => Value::string("custom", head),
477 "detailed_type" => Value::string(value_type, head),
478 "subtype" => Value::string(val.type_name(), head),
479 "rust_type" => Value::string(type_of(&val), head),
480 "value" =>
481 match val.to_base_value(head) {
482 Ok(base_value) => base_value,
483 Err(err) => Value::error(err, head),
484 }
485 }),
486 }
487}
488
489fn metadata_to_value(metadata: Option<PipelineMetadata>, head: Span) -> Value {
490 if let Some(metadata) = metadata {
491 let data_source = Value::string(format!("{:?}", metadata.data_source), head);
492 Value::record(record! { "data_source" => data_source }, head)
493 } else {
494 Value::nothing(head)
495 }
496}
497
498#[cfg(test)]
499mod test {
500 #[test]
501 fn test_examples() {
502 use super::Describe;
503 use crate::test_examples;
504 test_examples(Describe {})
505 }
506}