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, },
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, },
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, },
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}