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            .param(
53                Flag::new("trim")
54                    .short('t')
55                    .arg(SyntaxShape::String)
56                    .desc(
57                        "drop leading and trailing whitespaces around headers names and/or field \
58                         values",
59                    )
60                    .completion(Completion::new_list(&["all", "fields", "headers", "none"])),
61            )
62            .category(Category::Formats)
63    }
64
65    fn description(&self) -> &str {
66        "Parse text as .csv and create table."
67    }
68
69    fn run(
70        &self,
71        engine_state: &EngineState,
72        stack: &mut Stack,
73        call: &Call,
74        input: PipelineData,
75    ) -> Result<PipelineData, ShellError> {
76        from_csv(engine_state, stack, call, input)
77    }
78
79    fn examples(&self) -> Vec<Example<'_>> {
80        vec![
81            Example {
82                description: "Convert comma-separated data to a table",
83                example: "\"ColA,ColB\n1,2\" | from csv",
84                result: Some(Value::test_list(vec![Value::test_record(record! {
85                    "ColA" => Value::test_int(1),
86                    "ColB" => Value::test_int(2),
87                })])),
88            },
89            Example {
90                description: "Convert comma-separated data to a table, allowing variable number of columns per row",
91                example: "\"ColA,ColB\n1,2\n3,4,5\n6\" | from csv --flexible",
92                result: Some(Value::test_list(vec![
93                    Value::test_record(record! {
94                        "ColA" => Value::test_int(1),
95                        "ColB" => Value::test_int(2),
96                    }),
97                    Value::test_record(record! {
98                        "ColA" => Value::test_int(3),
99                        "ColB" => Value::test_int(4),
100                        "column2" => Value::test_int(5),
101                    }),
102                    Value::test_record(record! {
103                        "ColA" => Value::test_int(6),
104                    }),
105                ])),
106            },
107            Example {
108                description: "Convert comma-separated data to a table, ignoring headers",
109                example: "open data.txt | from csv --noheaders",
110                result: None,
111            },
112            Example {
113                description: "Convert semicolon-separated data to a table",
114                example: "open data.txt | from csv --separator ';'",
115                result: None,
116            },
117            Example {
118                description: "Convert comma-separated data to a table, ignoring lines starting with '#'",
119                example: "open data.txt | from csv --comment '#'",
120                result: None,
121            },
122            Example {
123                description: "Convert comma-separated data to a table, dropping all possible whitespaces around header names and field values",
124                example: "open data.txt | from csv --trim all",
125                result: None,
126            },
127            Example {
128                description: "Convert comma-separated data to a table, dropping all possible whitespaces around header names",
129                example: "open data.txt | from csv --trim headers",
130                result: None,
131            },
132            Example {
133                description: "Convert comma-separated data to a table, dropping all possible whitespaces around field values",
134                example: "open data.txt | from csv --trim fields",
135                result: None,
136            },
137        ]
138    }
139}
140
141fn from_csv(
142    engine_state: &EngineState,
143    stack: &mut Stack,
144    call: &Call,
145    input: PipelineData,
146) -> Result<PipelineData, ShellError> {
147    let name = call.head;
148    if let PipelineData::Value(Value::List { .. }, _) = input {
149        return Err(ShellError::TypeMismatch {
150            err_message: "received list stream, did you forget to open file with --raw flag?"
151                .into(),
152            span: name,
153        });
154    }
155
156    let separator = match call.get_flag::<String>(engine_state, stack, "separator")? {
157        Some(sep) => {
158            if sep.len() == 1 {
159                sep.chars().next().unwrap_or(',')
160            } else if sep.len() == 4 {
161                let unicode_sep = u32::from_str_radix(&sep, 16);
162                char::from_u32(unicode_sep.unwrap_or(b'\x1f' as u32)).unwrap_or(',')
163            } else {
164                return Err(ShellError::NonUtf8Custom {
165                    msg: "separator should be a single char or a 4-byte unicode".into(),
166                    span: call.span(),
167                });
168            }
169        }
170        None => ',',
171    };
172    let comment = call
173        .get_flag(engine_state, stack, "comment")?
174        .map(|v: Value| v.as_char())
175        .transpose()?;
176    let quote = call
177        .get_flag(engine_state, stack, "quote")?
178        .map(|v: Value| v.as_char())
179        .transpose()?
180        .unwrap_or('"');
181    let escape = call
182        .get_flag(engine_state, stack, "escape")?
183        .map(|v: Value| v.as_char())
184        .transpose()?;
185    let no_infer = call.has_flag(engine_state, stack, "no-infer")?;
186    let noheaders = call.has_flag(engine_state, stack, "noheaders")?;
187    let flexible = call.has_flag(engine_state, stack, "flexible")?;
188    let trim = trim_from_str(call.get_flag(engine_state, stack, "trim")?)?;
189
190    let config = DelimitedReaderConfig {
191        separator,
192        comment,
193        quote,
194        escape,
195        noheaders,
196        flexible,
197        no_infer,
198        trim,
199    };
200
201    from_delimited_data(config, input, name)
202}
203
204#[cfg(test)]
205mod test {
206    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
207
208    use super::*;
209
210    use crate::Reject;
211    use crate::{Metadata, MetadataSet};
212
213    #[test]
214    fn test_examples() {
215        use crate::test_examples;
216
217        test_examples(FromCsv {})
218    }
219
220    #[test]
221    fn test_content_type_metadata() {
222        let mut engine_state = Box::new(EngineState::new());
223        let delta = {
224            let mut working_set = StateWorkingSet::new(&engine_state);
225
226            working_set.add_decl(Box::new(FromCsv {}));
227            working_set.add_decl(Box::new(Metadata {}));
228            working_set.add_decl(Box::new(MetadataSet {}));
229            working_set.add_decl(Box::new(Reject {}));
230
231            working_set.render()
232        };
233
234        engine_state
235            .merge_delta(delta)
236            .expect("Error merging delta");
237
238        let cmd = r#""a,b\n1,2" | metadata set --content-type 'text/csv' --datasource-ls | from csv | metadata | reject span | $in"#;
239        let result = eval_pipeline_without_terminal_expression(
240            cmd,
241            std::env::temp_dir().as_ref(),
242            &mut engine_state,
243        );
244        assert_eq!(
245            Value::test_record(record!("source" => Value::test_string("ls"))),
246            result.expect("There should be a result")
247        )
248    }
249}