nu_command/strings/split/
row.rs1use 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, ®ex, 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}