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