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: "$'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: "$'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: "$'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: "$'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: "$'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() -> nu_test_support::Result {
173        nu_test_support::test().examples(FromTsv)
174    }
175
176    #[test]
177    fn test_content_type_metadata() {
178        let mut engine_state = Box::new(EngineState::new());
179        let delta = {
180            let mut working_set = StateWorkingSet::new(&engine_state);
181
182            working_set.add_decl(Box::new(FromTsv {}));
183            working_set.add_decl(Box::new(Metadata {}));
184            working_set.add_decl(Box::new(MetadataSet {}));
185            working_set.add_decl(Box::new(Reject {}));
186
187            working_set.render()
188        };
189
190        engine_state
191            .merge_delta(delta)
192            .expect("Error merging delta");
193
194        let cmd = r#""a\tb\n1\t2" | metadata set --content-type 'text/tab-separated-values' --path-columns [name] | from tsv | metadata | reject span | $in"#;
195        let result = eval_pipeline_without_terminal_expression(
196            cmd,
197            std::env::temp_dir().as_ref(),
198            &mut engine_state,
199        );
200        assert_eq!(
201            Value::test_record(
202                record!("path_columns" => Value::test_list(vec![Value::test_string("name")]))
203            ),
204            result.expect("There should be a result")
205        )
206    }
207}