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