nu_command/formats/from/
tsv.rs

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}