nu_cmd_lang/core_commands/
describe.rs

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, // Give "Running external commands not supported" error
194                              // result: Some(Value::test_record(record!(
195                              //     "type" => Value::test_string("stream"),
196                              //     "origin" => Value::test_string("nushell"),
197                              //     "subtype" => Value::test_record(record!(
198                              //         "type" => Value::test_string("list"),
199                              //         "length" => Value::test_int(3),
200                              //         "values" => Value::test_list(vec![
201                              //             Value::test_string("int"),
202                              //             Value::test_string("int"),
203                              //             Value::test_string("int"),
204                              //         ])
205                              //     ))
206                              // ))),
207            },
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, // Give "Running external commands not supported" error
212                              // result: Some(Value::test_string("list<int> (stream)")),
213            },
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, // Give "Running external commands not supported" error
218                              // result: Some(Value::test_string("stream")),
219            },
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}