1use nu_engine::command_prelude::*;
2use nu_protocol::ast::PathMember;
3use std::fmt::Write as _;
4
5const YAML_11_BOOLEANS: &[&str] = &[
7 "y", "Y", "yes", "Yes", "YES", "n", "N", "no", "No", "NO", "on", "On", "ON", "off", "Off",
8 "OFF",
9];
10
11const YAML_SPECIAL_NUMERICS: &[&str] = &[
13 ".inf", ".Inf", ".INF", "-.inf", "-.Inf", "-.INF", ".nan", ".NaN", ".NAN",
14];
15
16#[derive(Clone)]
17pub struct ToYamlLike(&'static str);
18pub const TO_YAML: ToYamlLike = ToYamlLike("to yaml");
19pub const TO_YML: ToYamlLike = ToYamlLike("to yml");
20
21impl Command for ToYamlLike {
22 fn name(&self) -> &str {
23 self.0
24 }
25
26 fn signature(&self) -> Signature {
27 Signature::build(self.name())
28 .input_output_types(vec![(Type::Any, Type::String)])
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 "Convert table into .yaml/.yml text."
39 }
40
41 fn examples(&self) -> Vec<Example<'_>> {
42 vec![Example {
43 description: "Outputs a YAML string representing the contents of this table.",
44 example: match self.name() {
45 "to yaml" => r#"[[foo bar]; ["1" "2"]] | to yaml"#,
46 "to yml" => r#"[[foo bar]; ["1" "2"]] | to yml"#,
47 _ => unreachable!("only implemented for `yaml` and `yml`"),
48 },
49 result: Some(Value::test_string("- foo: '1'\n bar: '2'\n")),
50 }]
51 }
52
53 fn run(
54 &self,
55 engine_state: &EngineState,
56 stack: &mut Stack,
57 call: &Call,
58 input: PipelineData,
59 ) -> Result<PipelineData, ShellError> {
60 let head = call.head;
61 let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
62 let input = input.try_expand_range()?;
63
64 to_yaml(engine_state, input, head, serialize_types)
65 }
66}
67
68pub fn value_to_yaml_value(
69 engine_state: &EngineState,
70 v: &Value,
71 serialize_types: bool,
72) -> Result<serde_yaml::Value, ShellError> {
73 Ok(match &v {
74 Value::Bool { val, .. } => serde_yaml::Value::Bool(*val),
75 Value::Int { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)),
76 Value::Filesize { val, .. } => {
77 serde_yaml::Value::Number(serde_yaml::Number::from(val.get()))
78 }
79 Value::Duration { val, .. } => serde_yaml::Value::String(val.to_string()),
80 Value::Date { val, .. } => serde_yaml::Value::String(val.to_string()),
81 Value::Range { .. } => serde_yaml::Value::Null,
82 Value::Float { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)),
83 Value::String { val, .. } | Value::Glob { val, .. } => {
84 serde_yaml::Value::String(val.clone())
85 }
86 Value::Record { val, .. } => {
87 let mut m = serde_yaml::Mapping::new();
88 for (k, v) in &**val {
89 m.insert(
90 serde_yaml::Value::String(k.clone()),
91 value_to_yaml_value(engine_state, v, serialize_types)?,
92 );
93 }
94 serde_yaml::Value::Mapping(m)
95 }
96 Value::List { vals, .. } => {
97 let mut out = vec![];
98
99 for value in vals {
100 out.push(value_to_yaml_value(engine_state, value, serialize_types)?);
101 }
102
103 serde_yaml::Value::Sequence(out)
104 }
105 Value::Closure { val, .. } => {
106 if serialize_types {
107 let block = engine_state.get_block(val.block_id);
108 if let Some(span) = block.span {
109 let contents_bytes = engine_state.get_span_contents(span);
110 let contents_string = String::from_utf8_lossy(contents_bytes);
111 serde_yaml::Value::String(contents_string.to_string())
112 } else {
113 serde_yaml::Value::String(format!(
114 "unable to retrieve block contents for yaml block_id {}",
115 val.block_id.get()
116 ))
117 }
118 } else {
119 serde_yaml::Value::Null
120 }
121 }
122 Value::Nothing { .. } => serde_yaml::Value::Null,
123 Value::Error { error, .. } => return Err(*error.clone()),
124 Value::Binary { val, .. } => serde_yaml::Value::Sequence(
125 val.iter()
126 .map(|x| serde_yaml::Value::Number(serde_yaml::Number::from(*x)))
127 .collect(),
128 ),
129 Value::CellPath { val, .. } => serde_yaml::Value::Sequence(
130 val.members
131 .iter()
132 .map(|x| match &x {
133 PathMember::String { val, .. } => Ok(serde_yaml::Value::String(val.clone())),
134 PathMember::Int { val, .. } => {
135 Ok(serde_yaml::Value::Number(serde_yaml::Number::from(*val)))
136 }
137 })
138 .collect::<Result<Vec<serde_yaml::Value>, ShellError>>()?,
139 ),
140 Value::Custom { .. } => serde_yaml::Value::Null,
141 })
142}
143
144fn render_yaml_string(value: &str) -> String {
145 if value.chars().any(char::is_control) {
146 let mut escaped = String::with_capacity(value.len() + 2);
147 escaped.push('"');
148
149 for ch in value.chars() {
150 match ch {
151 '"' => escaped.push_str("\\\""),
152 '\\' => escaped.push_str("\\\\"),
153 '\u{08}' => escaped.push_str("\\b"),
154 '\u{0C}' => escaped.push_str("\\f"),
155 '\n' => escaped.push_str("\\n"),
156 '\r' => escaped.push_str("\\r"),
157 '\t' => escaped.push_str("\\t"),
158 c if c.is_control() => {
159 let _ = write!(escaped, "\\u{:04X}", c as u32);
160 }
161 c => escaped.push(c),
162 }
163 }
164
165 escaped.push('"');
166 escaped
167 } else {
168 format!("'{}'", value.replace('\'', "''"))
169 }
170}
171
172fn should_quote_yaml_key(key: &str) -> bool {
173 if key.is_empty() {
174 return true;
175 }
176 if key.chars().any(char::is_control) {
177 return true;
178 }
179 if key.starts_with(char::is_whitespace) || key.ends_with(char::is_whitespace) {
180 return true;
181 }
182 if YAML_11_BOOLEANS.contains(&key) {
183 return true;
184 }
185 if matches!(
186 key,
187 "~" | "null" | "Null" | "NULL" | "true" | "True" | "TRUE" | "false" | "False" | "FALSE"
188 ) {
189 return true;
190 }
191 if YAML_SPECIAL_NUMERICS.contains(&key) {
193 return true;
194 }
195 if key.starts_with("0x") || key.starts_with("0X") {
196 return true;
197 }
198 if key.starts_with("0o") || key.starts_with("0O") {
199 return true;
200 }
201 if key.parse::<i64>().is_ok() {
202 return true;
203 }
204 if key.parse::<u64>().is_ok() {
205 return true;
206 }
207 if key.parse::<f64>().is_ok() {
208 return true;
209 }
210 if !key
211 .chars()
212 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/'))
213 {
214 return true;
215 }
216 false
217}
218
219fn render_yaml_key(key: &serde_yaml::Value) -> String {
220 match key {
221 serde_yaml::Value::String(key) if should_quote_yaml_key(key) => render_yaml_string(key),
222 serde_yaml::Value::String(key) => key.clone(),
223 _ => render_inline_yaml_value(key),
224 }
225}
226
227fn render_inline_yaml_value(value: &serde_yaml::Value) -> String {
228 match value {
229 serde_yaml::Value::Null => "null".to_string(),
230 serde_yaml::Value::Bool(value) => value.to_string(),
231 serde_yaml::Value::Number(value) => value.to_string(),
232 serde_yaml::Value::String(value) => render_yaml_string(value),
233 serde_yaml::Value::Sequence(values) => {
234 let values = values
235 .iter()
236 .map(render_inline_yaml_value)
237 .collect::<Vec<_>>()
238 .join(", ");
239 format!("[{values}]")
240 }
241 serde_yaml::Value::Mapping(entries) => {
242 let entries = entries
243 .iter()
244 .map(|(key, value)| {
245 format!(
246 "{}: {}",
247 render_yaml_key(key),
248 render_inline_yaml_value(value)
249 )
250 })
251 .collect::<Vec<_>>()
252 .join(", ");
253 format!("{{{entries}}}")
254 }
255 serde_yaml::Value::Tagged(tagged) => {
256 format!("{} {}", tagged.tag, render_inline_yaml_value(&tagged.value))
257 }
258 }
259}
260
261fn is_inline_yaml_value(value: &serde_yaml::Value) -> bool {
262 match value {
263 serde_yaml::Value::Sequence(values) => values.is_empty(),
264 serde_yaml::Value::Mapping(entries) => entries.is_empty(),
265 serde_yaml::Value::Tagged(tagged) => is_inline_yaml_value(&tagged.value),
266 _ => true,
267 }
268}
269
270fn write_yaml_indent(output: &mut String, indent: usize) {
271 for _ in 0..indent {
272 output.push(' ');
273 }
274}
275
276fn write_yaml_value(output: &mut String, value: &serde_yaml::Value, indent: usize) {
277 match value {
278 serde_yaml::Value::Sequence(values) if !values.is_empty() => {
279 write_yaml_sequence(output, values, indent);
280 }
281 serde_yaml::Value::Mapping(entries) if !entries.is_empty() => {
282 write_yaml_mapping(output, entries, indent, "");
283 }
284 serde_yaml::Value::Tagged(tagged) => write_yaml_value(output, &tagged.value, indent),
285 _ => {
286 write_yaml_indent(output, indent);
287 output.push_str(&render_inline_yaml_value(value));
288 output.push('\n');
289 }
290 }
291}
292
293fn write_yaml_sequence(output: &mut String, values: &[serde_yaml::Value], indent: usize) {
294 for value in values {
295 match value {
296 serde_yaml::Value::Mapping(entries) if !entries.is_empty() => {
297 write_yaml_mapping(output, entries, indent, "- ");
298 }
299 value if is_inline_yaml_value(value) => {
300 write_yaml_indent(output, indent);
301 output.push_str("- ");
302 output.push_str(&render_inline_yaml_value(value));
303 output.push('\n');
304 }
305 _ => {
306 write_yaml_indent(output, indent);
307 output.push_str("-\n");
308 write_yaml_value(output, value, indent + 2);
309 }
310 }
311 }
312}
313
314fn write_yaml_mapping(
315 output: &mut String,
316 entries: &serde_yaml::Mapping,
317 indent: usize,
318 first_prefix: &str,
319) {
320 let first_prefix_len = first_prefix.len();
321
322 for (index, (key, value)) in entries.iter().enumerate() {
323 let is_first = index == 0;
324 let line_indent = indent + if is_first { 0 } else { first_prefix_len };
328 let key_indent = line_indent + if is_first { first_prefix_len } else { 0 };
331
332 write_yaml_indent(output, line_indent);
333 if is_first {
334 output.push_str(first_prefix);
335 }
336
337 output.push_str(&render_yaml_key(key));
338
339 if is_inline_yaml_value(value) {
340 output.push_str(": ");
341 output.push_str(&render_inline_yaml_value(value));
342 output.push('\n');
343 } else {
344 output.push_str(":\n");
345 write_yaml_value(output, value, key_indent + 2);
346 }
347 }
348}
349
350fn yaml_value_to_string(value: &serde_yaml::Value) -> String {
351 let mut output = String::new();
352 write_yaml_value(&mut output, value, 0);
353 output
354}
355
356fn to_yaml(
357 engine_state: &EngineState,
358 mut input: PipelineData,
359 head: Span,
360 serialize_types: bool,
361) -> Result<PipelineData, ShellError> {
362 let metadata = input
363 .take_metadata()
364 .unwrap_or_default()
365 .with_content_type(Some("application/yaml".into()));
367 let value = input.into_value(head)?;
368
369 let yaml_value = value_to_yaml_value(engine_state, &value, serialize_types)?;
370 let yaml_string = yaml_value_to_string(&yaml_value);
371 Ok(Value::string(yaml_string, head).into_pipeline_data_with_metadata(Some(metadata)))
372}
373
374#[cfg(test)]
375mod test {
376 use super::*;
377 use crate::{Get, Metadata};
378 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
379
380 #[test]
381 fn test_examples() -> nu_test_support::Result {
382 nu_test_support::test().examples(TO_YAML)?;
383 nu_test_support::test().examples(TO_YML)
384 }
385
386 #[test]
387 fn test_content_type_metadata() {
388 let mut engine_state = Box::new(EngineState::new());
389 let delta = {
390 let mut working_set = StateWorkingSet::new(&engine_state);
393
394 working_set.add_decl(Box::new(TO_YAML));
395 working_set.add_decl(Box::new(Metadata {}));
396 working_set.add_decl(Box::new(Get {}));
397
398 working_set.render()
399 };
400
401 engine_state
402 .merge_delta(delta)
403 .expect("Error merging delta");
404
405 let cmd = "{a: 1 b: 2} | to yaml | metadata | get content_type | $in";
406 let result = eval_pipeline_without_terminal_expression(
407 cmd,
408 std::env::temp_dir().as_ref(),
409 &mut engine_state,
410 );
411 assert_eq!(
412 Value::test_string("application/yaml"),
413 result.expect("There should be a result")
414 );
415 }
416}