1use std::sync::Arc;
2
3use crate::formats::to::delimited::to_delimited_data;
4use nu_engine::command_prelude::*;
5use nu_protocol::Config;
6
7use super::delimited::ToDelimitedDataArgs;
8
9#[derive(Clone)]
10pub struct ToCsv;
11
12impl Command for ToCsv {
13 fn name(&self) -> &str {
14 "to csv"
15 }
16
17 fn signature(&self) -> Signature {
18 Signature::build("to csv")
19 .input_output_types(vec![
20 (Type::record(), Type::String),
21 (Type::table(), Type::String),
22 ])
23 .named(
24 "separator",
25 SyntaxShape::String,
26 "a character to separate columns, defaults to ','",
27 Some('s'),
28 )
29 .switch(
30 "noheaders",
31 "do not output the columns names as the first row",
32 Some('n'),
33 )
34 .named(
35 "columns",
36 SyntaxShape::List(SyntaxShape::String.into()),
37 "the names (in order) of the columns to use",
38 None,
39 )
40 .category(Category::Formats)
41 }
42
43 fn examples(&self) -> Vec<Example> {
44 vec![
45 Example {
46 description: "Outputs a CSV string representing the contents of this table",
47 example: "[[foo bar]; [1 2]] | to csv",
48 result: Some(Value::test_string("foo,bar\n1,2\n")),
49 },
50 Example {
51 description: "Outputs a CSV string representing the contents of this table",
52 example: "[[foo bar]; [1 2]] | to csv --separator ';' ",
53 result: Some(Value::test_string("foo;bar\n1;2\n")),
54 },
55 Example {
56 description: "Outputs a CSV string representing the contents of this record",
57 example: "{a: 1 b: 2} | to csv",
58 result: Some(Value::test_string("a,b\n1,2\n")),
59 },
60 Example {
61 description: "Outputs a CSV stream with column names pre-determined",
62 example: "[[foo bar baz]; [1 2 3]] | to csv --columns [baz foo]",
63 result: Some(Value::test_string("baz,foo\n3,1\n")),
64 },
65 ]
66 }
67
68 fn description(&self) -> &str {
69 "Convert table into .csv text ."
70 }
71
72 fn run(
73 &self,
74 engine_state: &EngineState,
75 stack: &mut Stack,
76 call: &Call,
77 input: PipelineData,
78 ) -> Result<PipelineData, ShellError> {
79 let head = call.head;
80 let noheaders = call.has_flag(engine_state, stack, "noheaders")?;
81 let separator: Option<Spanned<String>> = call.get_flag(engine_state, stack, "separator")?;
82 let columns: Option<Vec<String>> = call.get_flag(engine_state, stack, "columns")?;
83 let config = engine_state.config.clone();
84 to_csv(input, noheaders, separator, columns, head, config)
85 }
86}
87
88fn to_csv(
89 input: PipelineData,
90 noheaders: bool,
91 separator: Option<Spanned<String>>,
92 columns: Option<Vec<String>>,
93 head: Span,
94 config: Arc<Config>,
95) -> Result<PipelineData, ShellError> {
96 let sep = match separator {
97 Some(Spanned { item: s, span, .. }) => {
98 if s == r"\t" {
99 Spanned { item: '\t', span }
100 } else {
101 let vec_s: Vec<char> = s.chars().collect();
102 if vec_s.len() != 1 {
103 return Err(ShellError::TypeMismatch {
104 err_message: "Expected a single separator char from --separator"
105 .to_string(),
106 span,
107 });
108 };
109 Spanned {
110 item: vec_s[0],
111 span: head,
112 }
113 }
114 }
115 _ => Spanned {
116 item: ',',
117 span: head,
118 },
119 };
120
121 to_delimited_data(
122 ToDelimitedDataArgs {
123 noheaders,
124 separator: sep,
125 columns,
126 format_name: "CSV",
127 input,
128 head,
129 content_type: Some(mime::TEXT_CSV.to_string()),
130 },
131 config,
132 )
133}
134
135#[cfg(test)]
136mod test {
137
138 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
139
140 use crate::{Get, Metadata};
141
142 use super::*;
143
144 #[test]
145 fn test_examples() {
146 use crate::test_examples;
147 test_examples(ToCsv {})
148 }
149
150 #[test]
151 fn test_content_type_metadata() {
152 let mut engine_state = Box::new(EngineState::new());
153 let delta = {
154 let mut working_set = StateWorkingSet::new(&engine_state);
157
158 working_set.add_decl(Box::new(ToCsv {}));
159 working_set.add_decl(Box::new(Metadata {}));
160 working_set.add_decl(Box::new(Get {}));
161
162 working_set.render()
163 };
164
165 engine_state
166 .merge_delta(delta)
167 .expect("Error merging delta");
168
169 let cmd = "{a: 1 b: 2} | to csv | metadata | get content_type | $in";
170 let result = eval_pipeline_without_terminal_expression(
171 cmd,
172 std::env::temp_dir().as_ref(),
173 &mut engine_state,
174 );
175 assert_eq!(
176 Value::test_string("text/csv"),
177 result.expect("There should be a result")
178 );
179 }
180}