Skip to main content

nu_command/strings/split/
column.rs

1use fancy_regex::{Regex, escape};
2use nu_engine::command_prelude::*;
3
4use super::split;
5
6#[derive(Clone)]
7pub struct SplitColumn;
8
9impl Command for SplitColumn {
10    fn name(&self) -> &str {
11        "split column"
12    }
13
14    fn signature(&self) -> Signature {
15        Signature::build("split column")
16            .input_output_types(vec![
17                (Type::String, Type::table()),
18                (
19                    // TODO: no test coverage (is this behavior a bug or a feature?)
20                    Type::List(Box::new(Type::String)),
21                    Type::table(),
22                ),
23            ])
24            .required(
25                "separator",
26                SyntaxShape::String,
27                "The character or string that denotes what separates columns.",
28            )
29            .switch("collapse-empty", "Remove empty columns.", Some('c'))
30            .named(
31                "number",
32                SyntaxShape::Int,
33                "Split into maximum number of columns.",
34                Some('n'),
35            )
36            .switch(
37                "right",
38                "When `--number` is used, collect the remainder in the leftmost column.",
39                None,
40            )
41            .switch("regex", "Separator is a regular expression.", Some('r'))
42            .rest(
43                "rest",
44                SyntaxShape::String,
45                "Column names to give the new columns.",
46            )
47            .category(Category::Strings)
48    }
49
50    fn description(&self) -> &str {
51        "Split a string into multiple columns using a separator."
52    }
53
54    fn search_terms(&self) -> Vec<&str> {
55        vec!["separate", "divide", "regex"]
56    }
57
58    fn examples(&self) -> Vec<Example<'_>> {
59        vec![
60            Example {
61                description: "Split a string into columns by the specified separator.",
62                example: "'a--b--c' | split column '--'",
63                result: Some(Value::test_list(vec![Value::test_record(record! {
64                        "column0" => Value::test_string("a"),
65                        "column1" => Value::test_string("b"),
66                        "column2" => Value::test_string("c"),
67                })])),
68            },
69            Example {
70                description: "Split a string into columns of char and remove the empty columns.",
71                example: "'abc' | split column --collapse-empty ''",
72                result: Some(Value::test_list(vec![Value::test_record(record! {
73                        "column0" => Value::test_string("a"),
74                        "column1" => Value::test_string("b"),
75                        "column2" => Value::test_string("c"),
76                })])),
77            },
78            Example {
79                description: "Split a list of strings into a table.",
80                example: "['a-b' 'c-d'] | split column -",
81                result: Some(Value::test_list(vec![
82                    Value::test_record(record! {
83                        "column0" => Value::test_string("a"),
84                        "column1" => Value::test_string("b"),
85                    }),
86                    Value::test_record(record! {
87                        "column0" => Value::test_string("c"),
88                        "column1" => Value::test_string("d"),
89                    }),
90                ])),
91            },
92            Example {
93                description: "Split a list of strings into a table, ignoring padding.",
94                example: r"['a -  b' 'c  -    d'] | split column --regex '\s*-\s*'",
95                result: Some(Value::test_list(vec![
96                    Value::test_record(record! {
97                        "column0" => Value::test_string("a"),
98                        "column1" => Value::test_string("b"),
99                    }),
100                    Value::test_record(record! {
101                        "column0" => Value::test_string("c"),
102                        "column1" => Value::test_string("d"),
103                    }),
104                ])),
105            },
106            Example {
107                description: "Split into columns, last column may contain the delimiter.",
108                example: "['author: Salina Yoon' r#'title: Where's Ellie?: A Hide-and-Seek Book'#] | split column --number 2 ': ' key value",
109                result: Some(Value::test_list(vec![
110                    Value::test_record(record! {
111                        "key" => Value::test_string("author"),
112                        "value" => Value::test_string("Salina Yoon"),
113                    }),
114                    Value::test_record(record! {
115                        "key" => Value::test_string("title"),
116                        "value" => Value::test_string("Where's Ellie?: A Hide-and-Seek Book"),
117                    }),
118                ])),
119            },
120            Example {
121                description: "Split into columns, first column may contain the delimiter.",
122                example: "['some-package-1.2.3' 'pkg2-1.0' 'do-smart-things-0.9.1'] | split column --number 2 --right '-' name version",
123                result: Some(Value::test_list(vec![
124                    Value::test_record(record! {
125                        "name" => Value::test_string("some-package"),
126                        "version" => Value::test_string("1.2.3"),
127                    }),
128                    Value::test_record(record! {
129                        "name" => Value::test_string("pkg2"),
130                        "version" => Value::test_string("1.0"),
131                    }),
132                    Value::test_record(record! {
133                        "name" => Value::test_string("do-smart-things"),
134                        "version" => Value::test_string("0.9.1"),
135                    }),
136                ])),
137            },
138        ]
139    }
140
141    fn is_const(&self) -> bool {
142        true
143    }
144
145    fn run(
146        &self,
147        engine_state: &EngineState,
148        stack: &mut Stack,
149        call: &Call,
150        input: PipelineData,
151    ) -> Result<PipelineData, ShellError> {
152        let separator: Spanned<String> = call.req(engine_state, stack, 0)?;
153        let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 1)?;
154        let collapse_empty = call.has_flag(engine_state, stack, "collapse-empty")?;
155        let max_split: Option<usize> = call.get_flag(engine_state, stack, "number")?;
156        let split_from_right = call.has_flag(engine_state, stack, "right")?;
157        let has_regex = call.has_flag(engine_state, stack, "regex")?;
158
159        let args = Arguments {
160            separator,
161            rest,
162            collapse_empty,
163            max_split,
164            split_from_right,
165            has_regex,
166        };
167        split_column(engine_state, call, input, args)
168    }
169
170    fn run_const(
171        &self,
172        working_set: &StateWorkingSet,
173        call: &Call,
174        input: PipelineData,
175    ) -> Result<PipelineData, ShellError> {
176        let separator: Spanned<String> = call.req_const(working_set, 0)?;
177        let rest: Vec<Spanned<String>> = call.rest_const(working_set, 1)?;
178        let collapse_empty = call.has_flag_const(working_set, "collapse-empty")?;
179        let max_split: Option<usize> = call.get_flag_const(working_set, "number")?;
180        let split_from_right = call.has_flag_const(working_set, "right")?;
181        let has_regex = call.has_flag_const(working_set, "regex")?;
182
183        let args = Arguments {
184            separator,
185            rest,
186            collapse_empty,
187            max_split,
188            split_from_right,
189            has_regex,
190        };
191        split_column(working_set.permanent(), call, input, args)
192    }
193}
194
195struct Arguments {
196    separator: Spanned<String>,
197    rest: Vec<Spanned<String>>,
198    collapse_empty: bool,
199    max_split: Option<usize>,
200    split_from_right: bool,
201    has_regex: bool,
202}
203
204fn split_column(
205    engine_state: &EngineState,
206    call: &Call,
207    input: PipelineData,
208    args: Arguments,
209) -> Result<PipelineData, ShellError> {
210    let name_span = call.head;
211    let pattern = if args.has_regex {
212        std::borrow::Cow::Borrowed(args.separator.item.as_str())
213    } else {
214        escape(&args.separator.item)
215    };
216    let regex = engine_state.compile_regex(&pattern, args.separator.span)?;
217
218    input.flat_map(
219        move |x| {
220            split_column_helper(
221                &x,
222                &regex,
223                &args.rest,
224                args.collapse_empty,
225                args.max_split,
226                args.split_from_right,
227                name_span,
228            )
229        },
230        engine_state.signals(),
231    )
232}
233
234fn split_column_helper(
235    v: &Value,
236    separator: &Regex,
237    rest: &[Spanned<String>],
238    collapse_empty: bool,
239    max_split: Option<usize>,
240    split_from_right: bool,
241    head: Span,
242) -> Vec<Value> {
243    if let Ok(s) = v.as_str() {
244        let split_result: Vec<_> = match (max_split, split_from_right) {
245            (Some(0), _) => vec![],
246            (Some(max_split), true) => {
247                let sep_bounds: Vec<_> = separator
248                    .find_iter(s)
249                    .filter_map(|x| x.ok())
250                    .map(|x| (x.start(), x.end()))
251                    .collect();
252                // get the last `max_split` separators and split `s` with them
253                split(s, sep_bounds.into_iter().rev().take(max_split - 1).rev()).collect()
254            }
255            (Some(max_split), false) => separator
256                .splitn(s, max_split)
257                .filter_map(|x| x.ok())
258                .filter(|x| !(collapse_empty && x.is_empty()))
259                .collect(),
260            (None, _) => separator
261                .split(s)
262                .filter_map(|x| x.ok())
263                .filter(|x| !(collapse_empty && x.is_empty()))
264                .collect(),
265        };
266        let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect();
267
268        // If they didn't provide column names, make up our own
269        let mut record = Record::new();
270        if positional.is_empty() {
271            let mut gen_columns = vec![];
272            for i in 0..split_result.len() {
273                gen_columns.push(format!("column{}", i));
274            }
275
276            for (&k, v) in split_result.iter().zip(&gen_columns) {
277                record.push(v, Value::string(k, head));
278            }
279        } else {
280            for (&k, v) in split_result.iter().zip(&positional) {
281                record.push(v, Value::string(k, head));
282            }
283        }
284        vec![Value::record(record, head)]
285    } else {
286        match v {
287            Value::Error { error, .. } => {
288                vec![Value::error(*error.clone(), head)]
289            }
290            v => {
291                let span = v.span();
292                vec![Value::error(
293                    ShellError::OnlySupportsThisInputType {
294                        exp_input_type: "string".into(),
295                        wrong_type: v.get_type().to_string(),
296                        dst_span: head,
297                        src_span: span,
298                    },
299                    span,
300                )]
301            }
302        }
303    }
304}
305
306#[cfg(test)]
307mod test {
308    use super::*;
309
310    #[test]
311    fn test_examples() -> nu_test_support::Result {
312        nu_test_support::test().examples(SplitColumn)
313    }
314}