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}