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            .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}