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 .param(
45 Flag::new("trim")
46 .short('t')
47 .arg(SyntaxShape::String)
48 .desc(
49 "Drop leading and trailing whitespaces around headers names and/or field values.",
50 )
51 .completion(Completion::new_list(&["all", "fields", "headers", "none"])),
52 )
53 .category(Category::Formats)
54 }
55
56 fn description(&self) -> &str {
57 "Parse text as .tsv and create table."
58 }
59
60 fn run(
61 &self,
62 engine_state: &EngineState,
63 stack: &mut Stack,
64 call: &Call,
65 input: PipelineData,
66 ) -> Result<PipelineData, ShellError> {
67 from_tsv(engine_state, stack, call, input)
68 }
69
70 fn examples(&self) -> Vec<Example<'_>> {
71 vec![
72 Example {
73 description: "Convert tab-separated data to a table.",
74 example: "\"ColA\tColB\n1\t2\" | from tsv",
75 result: Some(Value::test_list(vec![Value::test_record(record! {
76 "ColA" => Value::test_int(1),
77 "ColB" => Value::test_int(2),
78 })])),
79 },
80 Example {
81 description: "Convert comma-separated data to a table, allowing variable number of columns per row and ignoring headers.",
82 example: "\"value 1\nvalue 2\tdescription 2\" | from tsv --flexible --noheaders",
83 result: Some(Value::test_list(vec![
84 Value::test_record(record! {
85 "column0" => Value::test_string("value 1"),
86 }),
87 Value::test_record(record! {
88 "column0" => Value::test_string("value 2"),
89 "column1" => Value::test_string("description 2"),
90 }),
91 ])),
92 },
93 Example {
94 description: "Create a tsv file with header columns and open it.",
95 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"#,
96 result: None,
97 },
98 Example {
99 description: "Create a tsv file without header columns and open it.",
100 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"#,
101 result: None,
102 },
103 Example {
104 description: "Create a tsv file without header columns and open it, removing all unnecessary whitespaces.",
105 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"#,
106 result: None,
107 },
108 Example {
109 description: "Create a tsv file without header columns and open it, removing all unnecessary whitespaces in the header names.",
110 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"#,
111 result: None,
112 },
113 Example {
114 description: "Create a tsv file without header columns and open it, removing all unnecessary whitespaces in the field values.",
115 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"#,
116 result: None,
117 },
118 ]
119 }
120}
121
122fn from_tsv(
123 engine_state: &EngineState,
124 stack: &mut Stack,
125 call: &Call,
126 input: PipelineData,
127) -> Result<PipelineData, ShellError> {
128 let name = call.head;
129
130 let comment = call
131 .get_flag(engine_state, stack, "comment")?
132 .map(|v: Value| v.as_char())
133 .transpose()?;
134 let quote = call
135 .get_flag(engine_state, stack, "quote")?
136 .map(|v: Value| v.as_char())
137 .transpose()?
138 .unwrap_or('"');
139 let escape = call
140 .get_flag(engine_state, stack, "escape")?
141 .map(|v: Value| v.as_char())
142 .transpose()?;
143 let no_infer = call.has_flag(engine_state, stack, "no-infer")?;
144 let noheaders = call.has_flag(engine_state, stack, "noheaders")?;
145 let flexible = call.has_flag(engine_state, stack, "flexible")?;
146 let trim = trim_from_str(call.get_flag(engine_state, stack, "trim")?)?;
147
148 let config = DelimitedReaderConfig {
149 separator: '\t',
150 comment,
151 quote,
152 escape,
153 noheaders,
154 flexible,
155 no_infer,
156 trim,
157 };
158
159 from_delimited_data(config, input, name)
160}
161
162#[cfg(test)]
163mod test {
164 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
165
166 use crate::Reject;
167 use crate::{Metadata, MetadataSet};
168
169 use super::*;
170
171 #[test]
172 fn test_examples() {
173 use crate::test_examples;
174
175 test_examples(FromTsv {})
176 }
177
178 #[test]
179 fn test_content_type_metadata() {
180 let mut engine_state = Box::new(EngineState::new());
181 let delta = {
182 let mut working_set = StateWorkingSet::new(&engine_state);
183
184 working_set.add_decl(Box::new(FromTsv {}));
185 working_set.add_decl(Box::new(Metadata {}));
186 working_set.add_decl(Box::new(MetadataSet {}));
187 working_set.add_decl(Box::new(Reject {}));
188
189 working_set.render()
190 };
191
192 engine_state
193 .merge_delta(delta)
194 .expect("Error merging delta");
195
196 let cmd = r#""a\tb\n1\t2" | metadata set --content-type 'text/tab-separated-values' --path-columns [name] | from tsv | metadata | reject span | $in"#;
197 let result = eval_pipeline_without_terminal_expression(
198 cmd,
199 std::env::temp_dir().as_ref(),
200 &mut engine_state,
201 );
202 assert_eq!(
203 Value::test_record(
204 record!("path_columns" => Value::test_list(vec![Value::test_string("name")]))
205 ),
206 result.expect("There should be a result")
207 )
208 }
209}