Skip to main content

nu_command/formats/to/
text.rs

1use chrono::Datelike;
2use chrono_humanize::HumanTime;
3use nu_engine::command_prelude::*;
4use nu_protocol::{ByteStream, PipelineMetadata, format_duration, shell_error::io::IoError};
5use nu_utils::{ObviousFloat, consts::LINE_SEPARATOR_STR};
6use std::io::Write;
7
8#[derive(Clone)]
9pub struct ToTextLike(&'static str);
10pub const TO_TEXT: ToTextLike = ToTextLike("to text");
11pub const TO_TXT: ToTextLike = ToTextLike("to txt");
12
13impl Command for ToTextLike {
14    fn name(&self) -> &str {
15        self.0
16    }
17
18    fn signature(&self) -> Signature {
19        Signature::build(self.name())
20            .input_output_types(vec![(Type::Any, Type::String)])
21            .switch(
22                "no-newline",
23                "Do not append a newline to the end of the text.",
24                Some('n'),
25            )
26            .switch(
27                "serialize",
28                "Serialize nushell types that cannot be deserialized.",
29                Some('s'),
30            )
31            .category(Category::Formats)
32    }
33
34    fn description(&self) -> &str {
35        "Convert data into plain text format."
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 head = call.head;
46        let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
47        let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
48        let input = input.try_expand_range()?;
49
50        match input {
51            PipelineData::Empty => Ok(Value::string(String::new(), head)
52                .into_pipeline_data_with_metadata(update_metadata(None))),
53            PipelineData::Value(value, ..) => {
54                let add_trailing = !no_newline
55                    && match &value {
56                        Value::List { vals, .. } => !vals.is_empty(),
57                        Value::Record { val, .. } => !val.is_empty(),
58                        _ => false,
59                    };
60                let mut str =
61                    local_into_string(engine_state, value, LINE_SEPARATOR_STR, serialize_types);
62                if add_trailing {
63                    str.push_str(LINE_SEPARATOR_STR);
64                }
65                Ok(
66                    Value::string(str, head)
67                        .into_pipeline_data_with_metadata(update_metadata(None)),
68                )
69            }
70            PipelineData::ListStream(stream, meta) => {
71                let span = stream.span();
72                let from_io_error = IoError::factory(head, None);
73                let stream = if no_newline {
74                    let mut first = true;
75                    let mut iter = stream.into_inner();
76                    let engine_state_clone = engine_state.clone();
77                    ByteStream::from_fn(
78                        span,
79                        engine_state.signals().clone(),
80                        ByteStreamType::String,
81                        move |buf| {
82                            let Some(val) = iter.next() else {
83                                return Ok(false);
84                            };
85                            if first {
86                                first = false;
87                            } else {
88                                write!(buf, "{LINE_SEPARATOR_STR}").map_err(&from_io_error)?;
89                            }
90                            // TODO: write directly into `buf` instead of creating an intermediate
91                            // string.
92                            let str = local_into_string(
93                                &engine_state_clone,
94                                val,
95                                LINE_SEPARATOR_STR,
96                                serialize_types,
97                            );
98                            write!(buf, "{str}").map_err(&from_io_error)?;
99                            Ok(true)
100                        },
101                    )
102                } else {
103                    let engine_state_clone = engine_state.clone();
104                    ByteStream::from_iter(
105                        stream.into_inner().map(move |val| {
106                            let mut str = local_into_string(
107                                &engine_state_clone,
108                                val,
109                                LINE_SEPARATOR_STR,
110                                serialize_types,
111                            );
112                            str.push_str(LINE_SEPARATOR_STR);
113                            str
114                        }),
115                        span,
116                        engine_state.signals().clone(),
117                        ByteStreamType::String,
118                    )
119                };
120
121                Ok(PipelineData::byte_stream(stream, update_metadata(meta)))
122            }
123            PipelineData::ByteStream(stream, meta) => {
124                Ok(PipelineData::byte_stream(stream, update_metadata(meta)))
125            }
126        }
127    }
128
129    fn examples(&self) -> Vec<Example<'_>> {
130        let command = self.name();
131        vec![
132            Example {
133                description: "Outputs data as simple text with a trailing newline.",
134                example: match command {
135                    "to text" => "[1] | to text",
136                    "to txt" => "[1] | to txt",
137                    _ => unreachable!("only implemented for `text` and `txt`"),
138                },
139                result: Some(Value::test_string("1".to_string() + LINE_SEPARATOR_STR)),
140            },
141            Example {
142                description: "Outputs data as simple text without a trailing newline.",
143                example: match command {
144                    "to text" => "[1] | to text --no-newline",
145                    "to txt" => "[1] | to txt --no-newline",
146                    _ => unreachable!("only implemented for `text` and `txt`"),
147                },
148                result: Some(Value::test_string("1")),
149            },
150            Example {
151                description: "Outputs external data as simple text.",
152                example: match command {
153                    "to text" => "git help -a | lines | find -r '^ ' | to text",
154                    "to txt" => "git help -a | lines | find -r '^ ' | to txt",
155                    _ => unreachable!("only implemented for `text` and `txt`"),
156                },
157                result: None,
158            },
159            Example {
160                description: "Outputs records as simple text.",
161                example: match command {
162                    "to text" => "ls | to text",
163                    "to txt" => "ls | to txt",
164                    _ => unreachable!("only implemented for `text` and `txt`"),
165                },
166                result: None,
167            },
168        ]
169    }
170}
171
172fn local_into_string(
173    engine_state: &EngineState,
174    value: Value,
175    separator: &str,
176    serialize_types: bool,
177) -> String {
178    let span = value.span();
179    match value {
180        Value::Bool { val, .. } => val.to_string(),
181        Value::Int { val, .. } => val.to_string(),
182        Value::Float { val, .. } => ObviousFloat(val).to_string(),
183        Value::Filesize { val, .. } => val.to_string(),
184        Value::Duration { val, .. } => format_duration(val, engine_state.config.duration_max_unit),
185        Value::Date { val, .. } => {
186            format!(
187                "{} ({})",
188                {
189                    if val.year() >= 0 && val.year() <= 9999 {
190                        val.to_rfc2822()
191                    } else {
192                        val.to_rfc3339()
193                    }
194                },
195                HumanTime::from(val)
196            )
197        }
198        Value::Range { val, .. } => val.to_string(),
199        Value::String { val, .. } => val,
200        Value::Glob { val, .. } => val,
201        Value::List { vals: val, .. } => val
202            .into_iter()
203            .map(|x| local_into_string(engine_state, x, ", ", serialize_types))
204            .collect::<Vec<_>>()
205            .join(separator),
206        Value::Record { val, .. } => val
207            .into_owned()
208            .into_iter()
209            .map(|(x, y)| {
210                format!(
211                    "{}: {}",
212                    x,
213                    local_into_string(engine_state, y, ", ", serialize_types)
214                )
215            })
216            .collect::<Vec<_>>()
217            .join(separator),
218        Value::Closure { val, .. } => {
219            if serialize_types {
220                let block = engine_state.get_block(val.block_id);
221                if let Some(span) = block.span {
222                    let contents_bytes = engine_state.get_span_contents(span);
223                    let contents_string = String::from_utf8_lossy(contents_bytes);
224                    contents_string.to_string()
225                } else {
226                    format!(
227                        "unable to retrieve block contents for text block_id {}",
228                        val.block_id.get()
229                    )
230                }
231            } else {
232                format!("closure_{}", val.block_id.get())
233            }
234        }
235        Value::Nothing { .. } => String::new(),
236        Value::Error { error, .. } => format!("{error:?}"),
237        Value::Binary { val, .. } => format!("{val:?}"),
238        Value::CellPath { val, .. } => val.to_string(),
239        // If we fail to collapse the custom value, just print <{type_name}> - failure is not
240        // that critical here
241        Value::Custom { val, .. } => val
242            .to_base_value(span)
243            .map(|val| local_into_string(engine_state, val, separator, serialize_types))
244            .unwrap_or_else(|_| format!("<{}>", val.type_name())),
245    }
246}
247
248fn update_metadata(metadata: Option<PipelineMetadata>) -> Option<PipelineMetadata> {
249    metadata
250        .map(|md| md.with_content_type(Some(mime::TEXT_PLAIN.to_string())))
251        .or_else(|| {
252            Some(PipelineMetadata::default().with_content_type(Some(mime::TEXT_PLAIN.to_string())))
253        })
254}
255
256#[cfg(test)]
257mod test {
258    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
259
260    use crate::{Get, Metadata};
261
262    use super::*;
263
264    #[test]
265    fn test_examples() -> nu_test_support::Result {
266        nu_test_support::test().examples(TO_TEXT)?;
267        nu_test_support::test().examples(TO_TXT)
268    }
269
270    #[test]
271    fn test_content_type_metadata() {
272        let mut engine_state = Box::new(EngineState::new());
273        let delta = {
274            // Base functions that are needed for testing
275            // Try to keep this working set small to keep tests running as fast as possible
276            let mut working_set = StateWorkingSet::new(&engine_state);
277
278            working_set.add_decl(Box::new(TO_TEXT));
279            working_set.add_decl(Box::new(Metadata {}));
280            working_set.add_decl(Box::new(Get {}));
281
282            working_set.render()
283        };
284
285        engine_state
286            .merge_delta(delta)
287            .expect("Error merging delta");
288
289        let cmd = "{a: 1 b: 2} | to text  | metadata | get content_type | $in";
290        let result = eval_pipeline_without_terminal_expression(
291            cmd,
292            std::env::temp_dir().as_ref(),
293            &mut engine_state,
294        );
295        assert_eq!(
296            Value::test_string("text/plain"),
297            result.expect("There should be a result")
298        );
299    }
300}