1use super::delimited::{DelimitedReaderConfig, from_delimited_data, trim_from_str};
2use nu_engine::command_prelude::*;
3
4#[derive(Clone)]
5pub struct FromTsv;
6
7impl Command for FromTsv {
8 fn name(&self) -> &str {
9 "from tsv"
10 }
11
12 fn signature(&self) -> Signature {
13 Signature::build("from tsv")
14 .input_output_types(vec![(Type::String, Type::table())])
15 .named(
16 "comment",
17 SyntaxShape::String,
18 "a comment character to ignore lines starting with it",
19 Some('c'),
20 )
21 .named(
22 "quote",
23 SyntaxShape::String,
24 "a quote character to ignore separators in strings, defaults to '\"'",
25 Some('q'),
26 )
27 .named(
28 "escape",
29 SyntaxShape::String,
30 "an escape character for strings containing the quote character",
31 Some('e'),
32 )
33 .switch(
34 "noheaders",
35 "don't treat the first row as column names",
36 Some('n'),
37 )
38 .switch(
39 "flexible",
40 "allow the number of fields in records to be variable",
41 None,
42 )
43 .switch("no-infer", "no field type inferencing", None)
44 .named(
45 "trim",
46 SyntaxShape::String,
47 "drop leading and trailing whitespaces around headers names and/or field values",
48 Some('t'),
49 )
50 .category(Category::Formats)
51 }
52
53 fn description(&self) -> &str {
54 "Parse text as .tsv and create table."
55 }
56
57 fn run(
58 &self,
59 engine_state: &EngineState,
60 stack: &mut Stack,
61 call: &Call,
62 input: PipelineData,
63 ) -> Result<PipelineData, ShellError> {
64 from_tsv(engine_state, stack, call, input)
65 }
66
67 fn examples(&self) -> Vec<Example> {
68 vec![
69 Example {
70 description: "Convert tab-separated data to a table",
71 example: "\"ColA\tColB\n1\t2\" | from tsv",
72 result: Some(Value::test_list(vec![Value::test_record(record! {
73 "ColA" => Value::test_int(1),
74 "ColB" => Value::test_int(2),
75 })])),
76 },
77 Example {
78 description: "Convert comma-separated data to a table, allowing variable number of columns per row and ignoring headers",
79 example: "\"value 1\nvalue 2\tdescription 2\" | from tsv --flexible --noheaders",
80 result: Some(Value::test_list(vec![
81 Value::test_record(record! {
82 "column0" => Value::test_string("value 1"),
83 }),
84 Value::test_record(record! {
85 "column0" => Value::test_string("value 2"),
86 "column1" => Value::test_string("description 2"),
87 }),
88 ])),
89 },
90 Example {
91 description: "Create a tsv file with header columns and open it",
92 example: r#"$'c1(char tab)c2(char tab)c3(char nl)1(char tab)2(char tab)3' | save tsv-data | open tsv-data | from tsv"#,
93 result: None,
94 },
95 Example {
96 description: "Create a tsv file without header columns and open it",
97 example: r#"$'a1(char tab)b1(char tab)c1(char nl)a2(char tab)b2(char tab)c2' | save tsv-data | open tsv-data | from tsv --noheaders"#,
98 result: None,
99 },
100 Example {
101 description: "Create a tsv file without header columns and open it, removing all unnecessary whitespaces",
102 example: r#"$'a1(char tab)b1(char tab)c1(char nl)a2(char tab)b2(char tab)c2' | save tsv-data | open tsv-data | from tsv --trim all"#,
103 result: None,
104 },
105 Example {
106 description: "Create a tsv file without header columns and open it, removing all unnecessary whitespaces in the header names",
107 example: r#"$'a1(char tab)b1(char tab)c1(char nl)a2(char tab)b2(char tab)c2' | save tsv-data | open tsv-data | from tsv --trim headers"#,
108 result: None,
109 },
110 Example {
111 description: "Create a tsv file without header columns and open it, removing all unnecessary whitespaces in the field values",
112 example: r#"$'a1(char tab)b1(char tab)c1(char nl)a2(char tab)b2(char tab)c2' | save tsv-data | open tsv-data | from tsv --trim fields"#,
113 result: None,
114 },
115 ]
116 }
117}
118
119fn from_tsv(
120 engine_state: &EngineState,
121 stack: &mut Stack,
122 call: &Call,
123 input: PipelineData,
124) -> Result<PipelineData, ShellError> {
125 let name = call.head;
126
127 let comment = call
128 .get_flag(engine_state, stack, "comment")?
129 .map(|v: Value| v.as_char())
130 .transpose()?;
131 let quote = call
132 .get_flag(engine_state, stack, "quote")?
133 .map(|v: Value| v.as_char())
134 .transpose()?
135 .unwrap_or('"');
136 let escape = call
137 .get_flag(engine_state, stack, "escape")?
138 .map(|v: Value| v.as_char())
139 .transpose()?;
140 let no_infer = call.has_flag(engine_state, stack, "no-infer")?;
141 let noheaders = call.has_flag(engine_state, stack, "noheaders")?;
142 let flexible = call.has_flag(engine_state, stack, "flexible")?;
143 let trim = trim_from_str(call.get_flag(engine_state, stack, "trim")?)?;
144
145 let config = DelimitedReaderConfig {
146 separator: '\t',
147 comment,
148 quote,
149 escape,
150 noheaders,
151 flexible,
152 no_infer,
153 trim,
154 };
155
156 from_delimited_data(config, input, name)
157}
158
159#[cfg(test)]
160mod test {
161 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
162
163 use crate::Reject;
164 use crate::{Metadata, MetadataSet};
165
166 use super::*;
167
168 #[test]
169 fn test_examples() {
170 use crate::test_examples;
171
172 test_examples(FromTsv {})
173 }
174
175 #[test]
176 fn test_content_type_metadata() {
177 let mut engine_state = Box::new(EngineState::new());
178 let delta = {
179 let mut working_set = StateWorkingSet::new(&engine_state);
180
181 working_set.add_decl(Box::new(FromTsv {}));
182 working_set.add_decl(Box::new(Metadata {}));
183 working_set.add_decl(Box::new(MetadataSet {}));
184 working_set.add_decl(Box::new(Reject {}));
185
186 working_set.render()
187 };
188
189 engine_state
190 .merge_delta(delta)
191 .expect("Error merging delta");
192
193 let cmd = r#""a\tb\n1\t2" | metadata set --content-type 'text/tab-separated-values' --datasource-ls | from tsv | metadata | reject span | $in"#;
194 let result = eval_pipeline_without_terminal_expression(
195 cmd,
196 std::env::temp_dir().as_ref(),
197 &mut engine_state,
198 );
199 assert_eq!(
200 Value::test_record(record!("source" => Value::test_string("ls"))),
201 result.expect("There should be a result")
202 )
203 }
204}