Skip to main content

nu_command/strings/split/
row.rs

1use fancy_regex::{Regex, escape};
2use nu_engine::command_prelude::*;
3use nu_protocol::shell_error::generic::GenericError;
4
5use super::split;
6
7#[derive(Clone)]
8pub struct SplitRow;
9
10impl Command for SplitRow {
11    fn name(&self) -> &str {
12        "split row"
13    }
14
15    fn signature(&self) -> Signature {
16        Signature::build("split row")
17            .input_output_types(vec![
18                (Type::String, Type::List(Box::new(Type::String))),
19                (
20                    Type::List(Box::new(Type::String)),
21                    (Type::List(Box::new(Type::String))),
22                ),
23            ])
24            .allow_variants_without_examples(true)
25            .required(
26                "separator",
27                SyntaxShape::String,
28                "A character or regex that denotes what separates rows.",
29            )
30            .named(
31                "number",
32                SyntaxShape::Int,
33                "Split into maximum number of items.",
34                Some('n'),
35            )
36            .switch(
37                "right",
38                "When `--number` is used, collect the remainder in the leftmost item.",
39                None,
40            )
41            .switch("regex", "Use regex syntax for separator.", Some('r'))
42            .category(Category::Strings)
43    }
44
45    fn description(&self) -> &str {
46        "Split a string into multiple rows using a separator."
47    }
48
49    fn search_terms(&self) -> Vec<&str> {
50        vec!["separate", "divide", "regex"]
51    }
52
53    fn examples(&self) -> Vec<Example<'_>> {
54        vec![
55            Example {
56                description: "Split a string into rows of char.",
57                example: "'abc' | split row ''",
58                result: Some(Value::list(
59                    vec![
60                        Value::test_string(""),
61                        Value::test_string("a"),
62                        Value::test_string("b"),
63                        Value::test_string("c"),
64                        Value::test_string(""),
65                    ],
66                    Span::test_data(),
67                )),
68            },
69            Example {
70                description: "Split a string into rows by the specified separator.",
71                example: "'a--b--c' | split row '--'",
72                result: Some(Value::list(
73                    vec![
74                        Value::test_string("a"),
75                        Value::test_string("b"),
76                        Value::test_string("c"),
77                    ],
78                    Span::test_data(),
79                )),
80            },
81            Example {
82                description: "Split a string by '-'.",
83                example: "'-a-b-c-' | split row '-'",
84                result: Some(Value::list(
85                    vec![
86                        Value::test_string(""),
87                        Value::test_string("a"),
88                        Value::test_string("b"),
89                        Value::test_string("c"),
90                        Value::test_string(""),
91                    ],
92                    Span::test_data(),
93                )),
94            },
95            Example {
96                description: "Split a string by regex.",
97                example: r"'a   b       c' | split row -r '\s+'",
98                result: Some(Value::list(
99                    vec![
100                        Value::test_string("a"),
101                        Value::test_string("b"),
102                        Value::test_string("c"),
103                    ],
104                    Span::test_data(),
105                )),
106            },
107            Example {
108                description: "Split a string a limited number of times.",
109                example: "'a-b-c' | split row --number 2 '-'",
110                result: Some(Value::list(
111                    vec![Value::test_string("a"), Value::test_string("b-c")],
112                    Span::test_data(),
113                )),
114            },
115            Example {
116                description: "Split a string a limited number of times, starting from the right.",
117                example: "'a-b-c' | split row --number 2 --right '-'",
118                result: Some(Value::list(
119                    vec![Value::test_string("a-b"), Value::test_string("c")],
120                    Span::test_data(),
121                )),
122            },
123        ]
124    }
125
126    fn is_const(&self) -> bool {
127        true
128    }
129
130    fn run(
131        &self,
132        engine_state: &EngineState,
133        stack: &mut Stack,
134        call: &Call,
135        input: PipelineData,
136    ) -> Result<PipelineData, ShellError> {
137        let separator: Spanned<String> = call.req(engine_state, stack, 0)?;
138        let max_split: Option<usize> = call.get_flag(engine_state, stack, "number")?;
139        let split_from_right = call.has_flag(engine_state, stack, "right")?;
140        let has_regex = call.has_flag(engine_state, stack, "regex")?;
141
142        let args = Arguments {
143            separator,
144            max_split,
145            split_from_right,
146            has_regex,
147        };
148        split_row(engine_state, call, input, args)
149    }
150
151    fn run_const(
152        &self,
153        working_set: &StateWorkingSet,
154        call: &Call,
155        input: PipelineData,
156    ) -> Result<PipelineData, ShellError> {
157        let separator: Spanned<String> = call.req_const(working_set, 0)?;
158        let max_split: Option<usize> = call.get_flag_const(working_set, "number")?;
159        let split_from_right = call.has_flag_const(working_set, "right")?;
160        let has_regex = call.has_flag_const(working_set, "regex")?;
161
162        let args = Arguments {
163            separator,
164            max_split,
165            split_from_right,
166            has_regex,
167        };
168        split_row(working_set.permanent(), call, input, args)
169    }
170}
171
172struct Arguments {
173    has_regex: bool,
174    separator: Spanned<String>,
175    max_split: Option<usize>,
176    split_from_right: bool,
177}
178
179fn split_row(
180    engine_state: &EngineState,
181    call: &Call,
182    input: PipelineData,
183    args: Arguments,
184) -> Result<PipelineData, ShellError> {
185    let name_span = call.head;
186    let regex = if args.has_regex {
187        Regex::new(&args.separator.item)
188    } else {
189        let escaped = escape(&args.separator.item);
190        Regex::new(&escaped)
191    }
192    .map_err(|e| {
193        ShellError::Generic(GenericError::new(
194            "Error with regular expression",
195            e.to_string(),
196            args.separator.span,
197        ))
198    })?;
199    input.flat_map(
200        move |x| split_row_helper(&x, &regex, args.max_split, args.split_from_right, name_span),
201        engine_state.signals(),
202    )
203}
204
205fn split_row_helper(
206    v: &Value,
207    regex: &Regex,
208    max_split: Option<usize>,
209    split_from_right: bool,
210    name: Span,
211) -> Vec<Value> {
212    let span = v.span();
213    if let Value::Error { error, .. } = v {
214        return vec![Value::error(*error.clone(), span)];
215    }
216    let Ok(s) = v.coerce_str() else {
217        return vec![Value::error(
218            ShellError::OnlySupportsThisInputType {
219                exp_input_type: "string".into(),
220                wrong_type: v.get_type().to_string(),
221                dst_span: name,
222                src_span: span,
223            },
224            name,
225        )];
226    };
227
228    match (max_split, split_from_right) {
229        (Some(0), _) => Ok(vec![]),
230        (Some(max_split), true) => regex
231            .find_iter(&s)
232            .map(|x| x.map(|x| (x.start(), x.end())))
233            .collect::<Result<Vec<_>, _>>()
234            .map(|sep_bounds| {
235                split(&s, sep_bounds.into_iter().rev().take(max_split - 1).rev())
236                    .map(|val| Value::string(val, span))
237                    .collect()
238            }),
239        (Some(max_split), false) => regex
240            .splitn(&s, max_split)
241            .map(|x| x.map(|val| Value::string(val, span)))
242            .collect(),
243        (None, _) => regex
244            .split(&s)
245            .map(|x| x.map(|val| Value::string(val, span)))
246            .collect(),
247    }
248    .unwrap_or_else(|err| {
249        vec![Value::error(
250            ShellError::Generic(GenericError::new(
251                "Error executing regular expression",
252                err.to_string(),
253                span,
254            )),
255            span,
256        )]
257    })
258}
259
260#[cfg(test)]
261mod test {
262    use super::*;
263
264    #[test]
265    fn test_examples() -> nu_test_support::Result {
266        nu_test_support::test().examples(SplitRow)
267    }
268}