1use fancy_regex::{Regex, escape};
2use nu_engine::command_prelude::*;
3
4use super::split;
5
6#[derive(Clone)]
7pub struct SplitColumn;
8
9impl Command for SplitColumn {
10 fn name(&self) -> &str {
11 "split column"
12 }
13
14 fn signature(&self) -> Signature {
15 Signature::build("split column")
16 .input_output_types(vec![
17 (Type::String, Type::table()),
18 (
19 Type::List(Box::new(Type::String)),
21 Type::table(),
22 ),
23 ])
24 .required(
25 "separator",
26 SyntaxShape::String,
27 "The character or string that denotes what separates columns.",
28 )
29 .switch("collapse-empty", "Remove empty columns.", Some('c'))
30 .named(
31 "number",
32 SyntaxShape::Int,
33 "Split into maximum number of columns.",
34 Some('n'),
35 )
36 .switch(
37 "right",
38 "When `--number` is used, collect the remainder in the leftmost column.",
39 None,
40 )
41 .switch("regex", "Separator is a regular expression.", Some('r'))
42 .rest(
43 "rest",
44 SyntaxShape::String,
45 "Column names to give the new columns.",
46 )
47 .category(Category::Strings)
48 }
49
50 fn description(&self) -> &str {
51 "Split a string into multiple columns using a separator."
52 }
53
54 fn search_terms(&self) -> Vec<&str> {
55 vec!["separate", "divide", "regex"]
56 }
57
58 fn examples(&self) -> Vec<Example<'_>> {
59 vec![
60 Example {
61 description: "Split a string into columns by the specified separator.",
62 example: "'a--b--c' | split column '--'",
63 result: Some(Value::test_list(vec![Value::test_record(record! {
64 "column0" => Value::test_string("a"),
65 "column1" => Value::test_string("b"),
66 "column2" => Value::test_string("c"),
67 })])),
68 },
69 Example {
70 description: "Split a string into columns of char and remove the empty columns.",
71 example: "'abc' | split column --collapse-empty ''",
72 result: Some(Value::test_list(vec![Value::test_record(record! {
73 "column0" => Value::test_string("a"),
74 "column1" => Value::test_string("b"),
75 "column2" => Value::test_string("c"),
76 })])),
77 },
78 Example {
79 description: "Split a list of strings into a table.",
80 example: "['a-b' 'c-d'] | split column -",
81 result: Some(Value::test_list(vec![
82 Value::test_record(record! {
83 "column0" => Value::test_string("a"),
84 "column1" => Value::test_string("b"),
85 }),
86 Value::test_record(record! {
87 "column0" => Value::test_string("c"),
88 "column1" => Value::test_string("d"),
89 }),
90 ])),
91 },
92 Example {
93 description: "Split a list of strings into a table, ignoring padding.",
94 example: r"['a - b' 'c - d'] | split column --regex '\s*-\s*'",
95 result: Some(Value::test_list(vec![
96 Value::test_record(record! {
97 "column0" => Value::test_string("a"),
98 "column1" => Value::test_string("b"),
99 }),
100 Value::test_record(record! {
101 "column0" => Value::test_string("c"),
102 "column1" => Value::test_string("d"),
103 }),
104 ])),
105 },
106 Example {
107 description: "Split into columns, last column may contain the delimiter.",
108 example: "['author: Salina Yoon' r#'title: Where's Ellie?: A Hide-and-Seek Book'#] | split column --number 2 ': ' key value",
109 result: Some(Value::test_list(vec![
110 Value::test_record(record! {
111 "key" => Value::test_string("author"),
112 "value" => Value::test_string("Salina Yoon"),
113 }),
114 Value::test_record(record! {
115 "key" => Value::test_string("title"),
116 "value" => Value::test_string("Where's Ellie?: A Hide-and-Seek Book"),
117 }),
118 ])),
119 },
120 Example {
121 description: "Split into columns, first column may contain the delimiter.",
122 example: "['some-package-1.2.3' 'pkg2-1.0' 'do-smart-things-0.9.1'] | split column --number 2 --right '-' name version",
123 result: Some(Value::test_list(vec![
124 Value::test_record(record! {
125 "name" => Value::test_string("some-package"),
126 "version" => Value::test_string("1.2.3"),
127 }),
128 Value::test_record(record! {
129 "name" => Value::test_string("pkg2"),
130 "version" => Value::test_string("1.0"),
131 }),
132 Value::test_record(record! {
133 "name" => Value::test_string("do-smart-things"),
134 "version" => Value::test_string("0.9.1"),
135 }),
136 ])),
137 },
138 ]
139 }
140
141 fn is_const(&self) -> bool {
142 true
143 }
144
145 fn run(
146 &self,
147 engine_state: &EngineState,
148 stack: &mut Stack,
149 call: &Call,
150 input: PipelineData,
151 ) -> Result<PipelineData, ShellError> {
152 let separator: Spanned<String> = call.req(engine_state, stack, 0)?;
153 let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 1)?;
154 let collapse_empty = call.has_flag(engine_state, stack, "collapse-empty")?;
155 let max_split: Option<usize> = call.get_flag(engine_state, stack, "number")?;
156 let split_from_right = call.has_flag(engine_state, stack, "right")?;
157 let has_regex = call.has_flag(engine_state, stack, "regex")?;
158
159 let args = Arguments {
160 separator,
161 rest,
162 collapse_empty,
163 max_split,
164 split_from_right,
165 has_regex,
166 };
167 split_column(engine_state, call, input, args)
168 }
169
170 fn run_const(
171 &self,
172 working_set: &StateWorkingSet,
173 call: &Call,
174 input: PipelineData,
175 ) -> Result<PipelineData, ShellError> {
176 let separator: Spanned<String> = call.req_const(working_set, 0)?;
177 let rest: Vec<Spanned<String>> = call.rest_const(working_set, 1)?;
178 let collapse_empty = call.has_flag_const(working_set, "collapse-empty")?;
179 let max_split: Option<usize> = call.get_flag_const(working_set, "number")?;
180 let split_from_right = call.has_flag_const(working_set, "right")?;
181 let has_regex = call.has_flag_const(working_set, "regex")?;
182
183 let args = Arguments {
184 separator,
185 rest,
186 collapse_empty,
187 max_split,
188 split_from_right,
189 has_regex,
190 };
191 split_column(working_set.permanent(), call, input, args)
192 }
193}
194
195struct Arguments {
196 separator: Spanned<String>,
197 rest: Vec<Spanned<String>>,
198 collapse_empty: bool,
199 max_split: Option<usize>,
200 split_from_right: bool,
201 has_regex: bool,
202}
203
204fn split_column(
205 engine_state: &EngineState,
206 call: &Call,
207 input: PipelineData,
208 args: Arguments,
209) -> Result<PipelineData, ShellError> {
210 let name_span = call.head;
211 let pattern = if args.has_regex {
212 std::borrow::Cow::Borrowed(args.separator.item.as_str())
213 } else {
214 escape(&args.separator.item)
215 };
216 let regex = engine_state.compile_regex(&pattern, args.separator.span)?;
217
218 input.flat_map(
219 move |x| {
220 split_column_helper(
221 &x,
222 ®ex,
223 &args.rest,
224 args.collapse_empty,
225 args.max_split,
226 args.split_from_right,
227 name_span,
228 )
229 },
230 engine_state.signals(),
231 )
232}
233
234fn split_column_helper(
235 v: &Value,
236 separator: &Regex,
237 rest: &[Spanned<String>],
238 collapse_empty: bool,
239 max_split: Option<usize>,
240 split_from_right: bool,
241 head: Span,
242) -> Vec<Value> {
243 if let Ok(s) = v.as_str() {
244 let split_result: Vec<_> = match (max_split, split_from_right) {
245 (Some(0), _) => vec![],
246 (Some(max_split), true) => {
247 let sep_bounds: Vec<_> = separator
248 .find_iter(s)
249 .filter_map(|x| x.ok())
250 .map(|x| (x.start(), x.end()))
251 .collect();
252 split(s, sep_bounds.into_iter().rev().take(max_split - 1).rev()).collect()
254 }
255 (Some(max_split), false) => separator
256 .splitn(s, max_split)
257 .filter_map(|x| x.ok())
258 .filter(|x| !(collapse_empty && x.is_empty()))
259 .collect(),
260 (None, _) => separator
261 .split(s)
262 .filter_map(|x| x.ok())
263 .filter(|x| !(collapse_empty && x.is_empty()))
264 .collect(),
265 };
266 let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect();
267
268 let mut record = Record::new();
270 if positional.is_empty() {
271 let mut gen_columns = vec![];
272 for i in 0..split_result.len() {
273 gen_columns.push(format!("column{}", i));
274 }
275
276 for (&k, v) in split_result.iter().zip(&gen_columns) {
277 record.push(v, Value::string(k, head));
278 }
279 } else {
280 for (&k, v) in split_result.iter().zip(&positional) {
281 record.push(v, Value::string(k, head));
282 }
283 }
284 vec![Value::record(record, head)]
285 } else {
286 match v {
287 Value::Error { error, .. } => {
288 vec![Value::error(*error.clone(), head)]
289 }
290 v => {
291 let span = v.span();
292 vec![Value::error(
293 ShellError::OnlySupportsThisInputType {
294 exp_input_type: "string".into(),
295 wrong_type: v.get_type().to_string(),
296 dst_span: head,
297 src_span: span,
298 },
299 span,
300 )]
301 }
302 }
303 }
304}
305
306#[cfg(test)]
307mod test {
308 use super::*;
309
310 #[test]
311 fn test_examples() -> nu_test_support::Result {
312 nu_test_support::test().examples(SplitColumn)
313 }
314}