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