Skip to main content

nu_cmd_lang/core_commands/
describe.rs

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