Skip to main content

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