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