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 pattern = if args.has_regex {
187        std::borrow::Cow::Borrowed(args.separator.item.as_str())
188    } else {
189        escape(&args.separator.item)
190    };
191    let regex = engine_state.compile_regex(&pattern, args.separator.span)?;
192    input.flat_map(
193        move |x| split_row_helper(&x, &regex, args.max_split, args.split_from_right, name_span),
194        engine_state.signals(),
195    )
196}
197
198fn split_row_helper(
199    v: &Value,
200    regex: &Regex,
201    max_split: Option<usize>,
202    split_from_right: bool,
203    name: Span,
204) -> Vec<Value> {
205    let span = v.span();
206    if let Value::Error { error, .. } = v {
207        return vec![Value::error(*error.clone(), span)];
208    }
209    let Ok(s) = v.coerce_string() else {
210        return vec![Value::error(
211            ShellError::OnlySupportsThisInputType {
212                exp_input_type: "string".into(),
213                wrong_type: v.get_type().to_string(),
214                dst_span: name,
215                src_span: span,
216            },
217            name,
218        )];
219    };
220
221    match (max_split, split_from_right) {
222        (Some(0), _) => Ok(vec![]),
223        (Some(max_split), true) => regex
224            .find_iter(&s)
225            .map(|x| x.map(|x| (x.start(), x.end())))
226            .collect::<Result<Vec<_>, _>>()
227            .map(|sep_bounds| {
228                split(&s, sep_bounds.into_iter().rev().take(max_split - 1).rev())
229                    .map(|val| Value::string(val, span))
230                    .collect()
231            }),
232        (Some(max_split), false) => regex
233            .splitn(&s, max_split)
234            .map(|x| x.map(|val| Value::string(val, span)))
235            .collect(),
236        (None, _) => regex
237            .split(&s)
238            .map(|x| x.map(|val| Value::string(val, span)))
239            .collect(),
240    }
241    .unwrap_or_else(|err| {
242        vec![Value::error(
243            ShellError::Generic(GenericError::new(
244                "Error executing regular expression",
245                err.to_string(),
246                span,
247            )),
248            span,
249        )]
250    })
251}
252
253#[cfg(test)]
254mod test {
255    use super::*;
256
257    #[test]
258    fn test_examples() -> nu_test_support::Result {
259        nu_test_support::test().examples(SplitRow)
260    }
261}