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}