nu_command/formats/from/
csv.rs

1use super::delimited::{DelimitedReaderConfig, from_delimited_data, trim_from_str};
2use nu_engine::command_prelude::*;
3
4#[derive(Clone)]
5pub struct FromCsv;
6
7impl Command for FromCsv {
8    fn name(&self) -> &str {
9        "from csv"
10    }
11
12    fn signature(&self) -> Signature {
13        Signature::build("from csv")
14            .input_output_types(vec![
15                (Type::String, Type::table()),
16            ])
17            .named(
18                "separator",
19                SyntaxShape::String,
20                "a character to separate columns (either single char or 4 byte unicode sequence), defaults to ','",
21                Some('s'),
22            )
23            .named(
24                "comment",
25                SyntaxShape::String,
26                "a comment character to ignore lines starting with it",
27                Some('c'),
28            )
29            .named(
30                "quote",
31                SyntaxShape::String,
32                "a quote character to ignore separators in strings, defaults to '\"'",
33                Some('q'),
34            )
35            .named(
36                "escape",
37                SyntaxShape::String,
38                "an escape character for strings containing the quote character",
39                Some('e'),
40            )
41            .switch(
42                "noheaders",
43                "don't treat the first row as column names",
44                Some('n'),
45            )
46            .switch(
47                "flexible",
48                "allow the number of fields in records to be variable",
49                None,
50            )
51            .switch("no-infer", "no field type inferencing", None)
52            .named(
53                "trim",
54                SyntaxShape::String,
55                "drop leading and trailing whitespaces around headers names and/or field values",
56                Some('t'),
57            )
58            .category(Category::Formats)
59    }
60
61    fn description(&self) -> &str {
62        "Parse text as .csv and create table."
63    }
64
65    fn run(
66        &self,
67        engine_state: &EngineState,
68        stack: &mut Stack,
69        call: &Call,
70        input: PipelineData,
71    ) -> Result<PipelineData, ShellError> {
72        from_csv(engine_state, stack, call, input)
73    }
74
75    fn examples(&self) -> Vec<Example> {
76        vec![
77            Example {
78                description: "Convert comma-separated data to a table",
79                example: "\"ColA,ColB\n1,2\" | from csv",
80                result: Some(Value::test_list(vec![Value::test_record(record! {
81                    "ColA" => Value::test_int(1),
82                    "ColB" => Value::test_int(2),
83                })])),
84            },
85            Example {
86                description: "Convert comma-separated data to a table, allowing variable number of columns per row",
87                example: "\"ColA,ColB\n1,2\n3,4,5\n6\" | from csv --flexible",
88                result: Some(Value::test_list(vec![
89                    Value::test_record(record! {
90                        "ColA" => Value::test_int(1),
91                        "ColB" => Value::test_int(2),
92                    }),
93                    Value::test_record(record! {
94                        "ColA" => Value::test_int(3),
95                        "ColB" => Value::test_int(4),
96                        "column2" => Value::test_int(5),
97                    }),
98                    Value::test_record(record! {
99                        "ColA" => Value::test_int(6),
100                    }),
101                ])),
102            },
103            Example {
104                description: "Convert comma-separated data to a table, ignoring headers",
105                example: "open data.txt | from csv --noheaders",
106                result: None,
107            },
108            Example {
109                description: "Convert semicolon-separated data to a table",
110                example: "open data.txt | from csv --separator ';'",
111                result: None,
112            },
113            Example {
114                description: "Convert comma-separated data to a table, ignoring lines starting with '#'",
115                example: "open data.txt | from csv --comment '#'",
116                result: None,
117            },
118            Example {
119                description: "Convert comma-separated data to a table, dropping all possible whitespaces around header names and field values",
120                example: "open data.txt | from csv --trim all",
121                result: None,
122            },
123            Example {
124                description: "Convert comma-separated data to a table, dropping all possible whitespaces around header names",
125                example: "open data.txt | from csv --trim headers",
126                result: None,
127            },
128            Example {
129                description: "Convert comma-separated data to a table, dropping all possible whitespaces around field values",
130                example: "open data.txt | from csv --trim fields",
131                result: None,
132            },
133        ]
134    }
135}
136
137fn from_csv(
138    engine_state: &EngineState,
139    stack: &mut Stack,
140    call: &Call,
141    input: PipelineData,
142) -> Result<PipelineData, ShellError> {
143    let name = call.head;
144    if let PipelineData::Value(Value::List { .. }, _) = input {
145        return Err(ShellError::TypeMismatch {
146            err_message: "received list stream, did you forget to open file with --raw flag?"
147                .into(),
148            span: name,
149        });
150    }
151
152    let separator = match call.get_flag::<String>(engine_state, stack, "separator")? {
153        Some(sep) => {
154            if sep.len() == 1 {
155                sep.chars().next().unwrap_or(',')
156            } else if sep.len() == 4 {
157                let unicode_sep = u32::from_str_radix(&sep, 16);
158                char::from_u32(unicode_sep.unwrap_or(b'\x1f' as u32)).unwrap_or(',')
159            } else {
160                return Err(ShellError::NonUtf8Custom {
161                    msg: "separator should be a single char or a 4-byte unicode".into(),
162                    span: call.span(),
163                });
164            }
165        }
166        None => ',',
167    };
168    let comment = call
169        .get_flag(engine_state, stack, "comment")?
170        .map(|v: Value| v.as_char())
171        .transpose()?;
172    let quote = call
173        .get_flag(engine_state, stack, "quote")?
174        .map(|v: Value| v.as_char())
175        .transpose()?
176        .unwrap_or('"');
177    let escape = call
178        .get_flag(engine_state, stack, "escape")?
179        .map(|v: Value| v.as_char())
180        .transpose()?;
181    let no_infer = call.has_flag(engine_state, stack, "no-infer")?;
182    let noheaders = call.has_flag(engine_state, stack, "noheaders")?;
183    let flexible = call.has_flag(engine_state, stack, "flexible")?;
184    let trim = trim_from_str(call.get_flag(engine_state, stack, "trim")?)?;
185
186    let config = DelimitedReaderConfig {
187        separator,
188        comment,
189        quote,
190        escape,
191        noheaders,
192        flexible,
193        no_infer,
194        trim,
195    };
196
197    from_delimited_data(config, input, name)
198}
199
200#[cfg(test)]
201mod test {
202    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
203
204    use super::*;
205
206    use crate::{Metadata, MetadataSet};
207
208    #[test]
209    fn test_examples() {
210        use crate::test_examples;
211
212        test_examples(FromCsv {})
213    }
214
215    #[test]
216    fn test_content_type_metadata() {
217        let mut engine_state = Box::new(EngineState::new());
218        let delta = {
219            let mut working_set = StateWorkingSet::new(&engine_state);
220
221            working_set.add_decl(Box::new(FromCsv {}));
222            working_set.add_decl(Box::new(Metadata {}));
223            working_set.add_decl(Box::new(MetadataSet {}));
224
225            working_set.render()
226        };
227
228        engine_state
229            .merge_delta(delta)
230            .expect("Error merging delta");
231
232        let cmd = r#""a,b\n1,2" | metadata set --content-type 'text/csv' --datasource-ls | from csv | metadata | $in"#;
233        let result = eval_pipeline_without_terminal_expression(
234            cmd,
235            std::env::temp_dir().as_ref(),
236            &mut engine_state,
237        );
238        assert_eq!(
239            Value::test_record(record!("source" => Value::test_string("ls"))),
240            result.expect("There should be a result")
241        )
242    }
243}