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