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, ®ex, 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}