nu_command/formats/to/
csv.rs

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            // Base functions that are needed for testing
155            // Try to keep this working set small to keep tests running as fast as possible
156            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}