1use indexmap::IndexMap;
2use nu_engine::{ClosureEval, command_prelude::*};
3use nu_protocol::{
4 FromValue, ast::PathMember, engine::Closure, shell_error::generic::GenericError,
5};
6
7#[derive(Clone)]
8pub struct GroupBy;
9
10impl Command for GroupBy {
11 fn name(&self) -> &str {
12 "group-by"
13 }
14
15 fn signature(&self) -> Signature {
16 Signature::build("group-by")
17 .input_output_types(vec![(Type::List(Box::new(Type::Any)), Type::Any)])
18 .switch(
19 "to-table",
20 "Return a table with \"groups\" and \"items\" columns.",
21 None,
22 )
23 .switch(
24 "prune",
25 "Remove a column after grouping, if applicable.",
26 None,
27 )
28 .rest(
29 "grouper",
30 SyntaxShape::OneOf(vec![
31 SyntaxShape::CellPath,
32 SyntaxShape::Closure(None),
33 SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
34 ]),
35 "The path to the column to group on.",
36 )
37 .category(Category::Filters)
38 }
39
40 fn description(&self) -> &str {
41 "Splits a list or table into groups, and returns a record containing those groups."
42 }
43
44 fn extra_description(&self) -> &str {
45 r#"the group-by command makes some assumptions:
46 - if the input data is not a string, the grouper will convert the key to string but the values will remain in their original format. e.g. with bools, "true" and true would be in the same group (see example).
47 - datetime is formatted based on your configuration setting. use `format date` to change the format.
48 - filesize is formatted based on your configuration setting. use `format filesize` to change the format.
49 - some nushell values are not supported, such as closures.
50 - null group keys are never mapped to the empty string. The default record output omits null groups (records cannot use null as a key); use --to-table to include them as null values. Optional cell paths (e.g. `foo?`) still ignore rows where access yields null."#
51 }
52
53 fn run(
54 &self,
55 engine_state: &EngineState,
56 stack: &mut Stack,
57 call: &Call,
58 input: PipelineData,
59 ) -> Result<PipelineData, ShellError> {
60 group_by(engine_state, stack, call, input)
61 }
62
63 fn examples(&self) -> Vec<Example<'_>> {
64 vec![
65 Example {
66 description: "Group items by the \"type\" column's values.",
67 example: "ls | group-by type",
68 result: None,
69 },
70 Example {
71 description: "Group items by the \"foo\" column's values, ignoring records without a \"foo\" column.",
72 example: "open cool.json | group-by foo?",
73 result: None,
74 },
75 Example {
76 description: "Group using a block which is evaluated against each input value.",
77 example: "[foo.txt bar.csv baz.txt] | group-by { path parse | get extension }",
78 result: Some(Value::test_record(record! {
79 "txt" => Value::test_list(vec![
80 Value::test_string("foo.txt"),
81 Value::test_string("baz.txt"),
82 ]),
83 "csv" => Value::test_list(vec![Value::test_string("bar.csv")]),
84 })),
85 },
86 Example {
87 description: "You can also group by raw values by leaving out the argument.",
88 example: "['1' '3' '1' '3' '2' '1' '1'] | group-by",
89 result: Some(Value::test_record(record! {
90 "1" => Value::test_list(vec![
91 Value::test_string("1"),
92 Value::test_string("1"),
93 Value::test_string("1"),
94 Value::test_string("1"),
95 ]),
96 "3" => Value::test_list(vec![
97 Value::test_string("3"),
98 Value::test_string("3"),
99 ]),
100 "2" => Value::test_list(vec![Value::test_string("2")]),
101 })),
102 },
103 Example {
104 description: "You can also output a table instead of a record.",
105 example: "['1' '3' '1' '3' '2' '1' '1'] | group-by --to-table",
106 result: Some(Value::test_list(vec![
107 Value::test_record(record! {
108 "group" => Value::test_string("1"),
109 "items" => Value::test_list(vec![
110 Value::test_string("1"),
111 Value::test_string("1"),
112 Value::test_string("1"),
113 Value::test_string("1"),
114 ]),
115 }),
116 Value::test_record(record! {
117 "group" => Value::test_string("3"),
118 "items" => Value::test_list(vec![
119 Value::test_string("3"),
120 Value::test_string("3"),
121 ]),
122 }),
123 Value::test_record(record! {
124 "group" => Value::test_string("2"),
125 "items" => Value::test_list(vec![Value::test_string("2")]),
126 }),
127 ])),
128 },
129 Example {
130 description: "Group bools, whether they are strings or actual bools.",
131 example: r#"[true "true" false "false"] | group-by"#,
132 result: Some(Value::test_record(record! {
133 "true" => Value::test_list(vec![
134 Value::test_bool(true),
135 Value::test_string("true"),
136 ]),
137 "false" => Value::test_list(vec![
138 Value::test_bool(false),
139 Value::test_string("false"),
140 ]),
141 })),
142 },
143 Example {
144 description: "Group items by multiple columns' values.",
145 example: r#"[
146 [name, lang, year];
147 [andres, rb, "2019"],
148 [jt, rs, "2019"],
149 [storm, rs, "2021"]
150 ]
151 | group-by lang year"#,
152 result: Some(Value::test_record(record! {
153 "rb" => Value::test_record(record! {
154 "2019" => Value::test_list(
155 vec![Value::test_record(record! {
156 "name" => Value::test_string("andres"),
157 "lang" => Value::test_string("rb"),
158 "year" => Value::test_string("2019"),
159 })],
160 ),
161 }),
162 "rs" => Value::test_record(record! {
163 "2019" => Value::test_list(
164 vec![Value::test_record(record! {
165 "name" => Value::test_string("jt"),
166 "lang" => Value::test_string("rs"),
167 "year" => Value::test_string("2019"),
168 })],
169 ),
170 "2021" => Value::test_list(
171 vec![Value::test_record(record! {
172 "name" => Value::test_string("storm"),
173 "lang" => Value::test_string("rs"),
174 "year" => Value::test_string("2021"),
175 })],
176 ),
177 }),
178 })),
179 },
180 Example {
181 description: "Group items by multiple columns' values.",
182 example: r#"[
183 [name, lang, year];
184 [andres, rb, "2019"],
185 [jt, rs, "2019"],
186 [storm, rs, "2021"]
187 ]
188 | group-by lang year --to-table"#,
189 result: Some(Value::test_list(vec![
190 Value::test_record(record! {
191 "lang" => Value::test_string("rb"),
192 "year" => Value::test_string("2019"),
193 "items" => Value::test_list(vec![
194 Value::test_record(record! {
195 "name" => Value::test_string("andres"),
196 "lang" => Value::test_string("rb"),
197 "year" => Value::test_string("2019"),
198 })
199 ]),
200 }),
201 Value::test_record(record! {
202 "lang" => Value::test_string("rs"),
203 "year" => Value::test_string("2019"),
204 "items" => Value::test_list(vec![
205 Value::test_record(record! {
206 "name" => Value::test_string("jt"),
207 "lang" => Value::test_string("rs"),
208 "year" => Value::test_string("2019"),
209 })
210 ]),
211 }),
212 Value::test_record(record! {
213 "lang" => Value::test_string("rs"),
214 "year" => Value::test_string("2021"),
215 "items" => Value::test_list(vec![
216 Value::test_record(record! {
217 "name" => Value::test_string("storm"),
218 "lang" => Value::test_string("rs"),
219 "year" => Value::test_string("2021"),
220 })
221 ]),
222 }),
223 ])),
224 },
225 Example {
226 description: "Group items by column and delete the original.",
227 example: r#"[
228 [name, lang, year];
229 [andres, rb, "2019"],
230 [jt, rs, "2019"],
231 [storm, rs, "2021"]
232 ]
233 | group-by lang --prune"#,
234 #[cfg(test)] result: None,
236 #[cfg(not(test))]
237 result: Some(Value::test_record(record! {
238 "rb" => Value::test_list(vec![Value::test_record(record! {
239 "name" => Value::test_string("andres"),
240 "year" => Value::test_string("2019"),
241 })],
242 ),
243 "rs" => Value::test_list(
244 vec![
245 Value::test_record(record! {
246 "name" => Value::test_string("jt"),
247 "year" => Value::test_string("2019"),
248 }),
249 Value::test_record(record! {
250 "name" => Value::test_string("storm"),
251 "year" => Value::test_string("2021"),
252 })
253 ]),
254 })),
255 },
256 ]
257 }
258}
259
260pub fn group_by(
261 engine_state: &EngineState,
262 stack: &mut Stack,
263 call: &Call,
264 input: PipelineData,
265) -> Result<PipelineData, ShellError> {
266 let head = call.head;
267 let groupers: Vec<Spanned<Grouper>> = call.rest(engine_state, stack, 0)?;
268 let to_table = call.has_flag(engine_state, stack, "to-table")?;
269 let prune = call.has_flag(engine_state, stack, "prune")?;
270 let config = &stack.get_config(engine_state);
271
272 let values: Vec<Value> = input.into_iter().collect();
273 if values.is_empty() {
274 let val = if to_table {
275 Value::list(Vec::new(), head)
276 } else {
277 Value::record(Record::new(), head)
278 };
279 return Ok(val.into_pipeline_data());
280 }
281
282 let grouped = match &groupers[..] {
283 [first, rest @ ..] => {
284 let mut grouped =
285 Grouped::new(first.as_ref(), prune, values, config, engine_state, stack)?;
286 for grouper in rest {
287 grouped.subgroup(grouper.as_ref(), prune, config, engine_state, stack)?;
288 }
289 grouped
290 }
291 [] => Grouped::empty(values, config),
292 };
293
294 let value = if to_table {
295 let column_names = groupers_to_column_names(&groupers)?;
296 grouped.into_table(&column_names, head)
297 } else {
298 grouped.into_record(head)
299 };
300
301 Ok(value.into_pipeline_data())
302}
303
304fn groupers_to_column_names(groupers: &[Spanned<Grouper>]) -> Result<Vec<String>, ShellError> {
305 if groupers.is_empty() {
306 return Ok(vec!["group".into(), "items".into()]);
307 }
308
309 let mut closure_idx: usize = 0;
310 let grouper_names = groupers.iter().map(|grouper| {
311 grouper.as_ref().map(|item| match item {
312 Grouper::CellPath { val } => val.to_column_name(),
313 Grouper::Closure { .. } => {
314 closure_idx += 1;
315 format!("closure_{}", closure_idx - 1)
316 }
317 })
318 });
319
320 let mut name_set: Vec<Spanned<String>> = Vec::with_capacity(grouper_names.len());
321
322 for name in grouper_names {
323 if name.item == "items" {
324 return Err(ShellError::Generic(
325 GenericError::new(
326 "grouper arguments can't be named `items`",
327 "here",
328 name.span,
329 )
330 .with_help("instead of a cell-path, try using a closure: { get items }"),
331 ));
332 }
333
334 if let Some(conflicting_name) = name_set
335 .iter()
336 .find(|elem| elem.as_ref().item == name.item.as_str())
337 {
338 return Err(ShellError::Generic(
339 GenericError::new(
340 "grouper arguments result in colliding column names",
341 "duplicate column names",
342 conflicting_name.span.append(name.span),
343 )
344 .with_help("instead of a cell-path, try using a closure or renaming columns")
345 .with_inner([ShellError::ColumnDefinedTwice {
346 col_name: conflicting_name.item.clone(),
347 first_use: conflicting_name.span,
348 second_use: name.span,
349 }]),
350 ));
351 }
352
353 name_set.push(name);
354 }
355
356 let column_names: Vec<String> = name_set
357 .into_iter()
358 .map(|elem| elem.item)
359 .chain(["items".into()])
360 .collect();
361 Ok(column_names)
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Hash)]
367enum GroupKey {
368 Nothing,
369 String(String),
370}
371
372impl GroupKey {
373 fn from_value(value: &Value, config: &nu_protocol::Config) -> Self {
374 if value.is_nothing() {
375 Self::Nothing
376 } else {
377 Self::String(value.to_expanded_string(", ", config))
378 }
379 }
380
381 fn into_value(self, span: Span) -> Value {
382 match self {
383 Self::Nothing => Value::nothing(span),
384 Self::String(s) => Value::string(s, span),
385 }
386 }
387}
388
389fn path_has_optional_member(column_name: &CellPath) -> bool {
390 column_name.members.iter().any(|member| match member {
391 PathMember::String { optional, .. } => *optional,
392 PathMember::Int { optional, .. } => *optional,
393 })
394}
395
396fn group_cell_path(
397 column_name: &CellPath,
398 prune: bool,
399 values: Vec<Value>,
400 config: &nu_protocol::Config,
401) -> Result<IndexMap<GroupKey, Vec<Value>>, ShellError> {
402 let mut groups = IndexMap::<_, Vec<_>>::new();
403 let optional_path = path_has_optional_member(column_name);
404
405 for mut value in values.into_iter() {
406 let key_val = value.follow_cell_path(&column_name.members)?;
407
408 if key_val.is_nothing() && optional_path {
411 continue;
412 }
413
414 let key = GroupKey::from_value(key_val.as_ref(), config);
415
416 if prune {
417 let _ = value.remove_data_at_cell_path(&column_name.members);
419
420 let parent = column_name.members.split_last().map(|(_, head)| head);
422
423 if let Some(parent) = parent
424 && let Ok(parent_value) = value.follow_cell_path(parent)
425 && parent_value.is_empty()
426 {
427 let _ = value.remove_data_at_cell_path(parent);
428 }
429 }
430
431 groups.entry(key).or_default().push(value);
432 }
433
434 Ok(groups)
435}
436
437fn group_closure(
438 values: Vec<Value>,
439 span: Span,
440 closure: Closure,
441 engine_state: &EngineState,
442 stack: &mut Stack,
443) -> Result<IndexMap<GroupKey, Vec<Value>>, ShellError> {
444 let mut groups = IndexMap::<_, Vec<_>>::new();
445 let mut closure = ClosureEval::new(engine_state, stack, closure);
446 let config = &stack.get_config(engine_state);
447
448 for value in values {
449 let key_val = closure.run_with_value(value.clone())?.into_value(span)?;
450 let key = GroupKey::from_value(&key_val, config);
451
452 groups.entry(key).or_default().push(value);
453 }
454
455 Ok(groups)
456}
457
458enum Grouper {
459 CellPath { val: CellPath },
460 Closure { val: Box<Closure> },
461}
462
463impl FromValue for Grouper {
464 fn from_value(v: Value) -> Result<Self, ShellError> {
465 match v {
466 Value::CellPath { val, .. } => Ok(Grouper::CellPath { val }),
467 Value::Closure { val, .. } => Ok(Grouper::Closure { val }),
468 _ => Err(ShellError::TypeMismatch {
469 err_message: "unsupported grouper type".to_string(),
470 span: v.span(),
471 }),
472 }
473 }
474}
475
476struct Grouped {
477 groups: Tree,
478}
479
480enum Tree {
481 Leaf(IndexMap<GroupKey, Vec<Value>>),
482 Branch(IndexMap<GroupKey, Grouped>),
483}
484
485impl Grouped {
486 fn empty(values: Vec<Value>, config: &nu_protocol::Config) -> Self {
487 let mut groups = IndexMap::<_, Vec<_>>::new();
488
489 for value in values.into_iter() {
490 let key = GroupKey::from_value(&value, config);
491 groups.entry(key).or_default().push(value);
492 }
493
494 Self {
495 groups: Tree::Leaf(groups),
496 }
497 }
498
499 fn new(
500 grouper: Spanned<&Grouper>,
501 prune: bool,
502 values: Vec<Value>,
503 config: &nu_protocol::Config,
504 engine_state: &EngineState,
505 stack: &mut Stack,
506 ) -> Result<Self, ShellError> {
507 let groups = match grouper.item {
508 Grouper::CellPath { val } => group_cell_path(val, prune, values, config)?,
509 Grouper::Closure { val } => group_closure(
510 values,
511 grouper.span,
512 Closure::clone(val),
513 engine_state,
514 stack,
515 )?,
516 };
517 Ok(Self {
518 groups: Tree::Leaf(groups),
519 })
520 }
521
522 fn subgroup(
523 &mut self,
524 grouper: Spanned<&Grouper>,
525 prune: bool,
526 config: &nu_protocol::Config,
527 engine_state: &EngineState,
528 stack: &mut Stack,
529 ) -> Result<(), ShellError> {
530 let groups = match &mut self.groups {
531 Tree::Leaf(groups) => std::mem::take(groups)
532 .into_iter()
533 .map(|(key, values)| -> Result<_, ShellError> {
534 let leaf = Self::new(grouper, prune, values, config, engine_state, stack)?;
535 Ok((key, leaf))
536 })
537 .collect::<Result<IndexMap<_, _>, ShellError>>()?,
538 Tree::Branch(nested_groups) => {
539 let mut nested_groups = std::mem::take(nested_groups);
540 for v in nested_groups.values_mut() {
541 v.subgroup(grouper, prune, config, engine_state, stack)?;
542 }
543 nested_groups
544 }
545 };
546 self.groups = Tree::Branch(groups);
547 Ok(())
548 }
549
550 fn into_table(self, column_names: &[String], head: Span) -> Value {
551 self._into_table(head)
552 .into_iter()
553 .map(|row| {
554 row.into_iter()
555 .rev()
556 .zip(column_names)
557 .map(|(val, key)| (key.clone(), val))
558 .collect::<Record>()
559 .into_value(head)
560 })
561 .collect::<Vec<_>>()
562 .into_value(head)
563 }
564
565 fn _into_table(self, head: Span) -> Vec<Vec<Value>> {
566 match self.groups {
567 Tree::Leaf(leaf) => leaf
568 .into_iter()
569 .map(|(group, values)| vec![values.into_value(head), group.into_value(head)])
570 .collect::<Vec<Vec<Value>>>(),
571 Tree::Branch(branch) => branch
572 .into_iter()
573 .flat_map(|(group, items)| {
574 let group_val = group.into_value(head);
575 let mut inner = items._into_table(head);
576 for row in &mut inner {
577 row.push(group_val.clone());
578 }
579 inner
580 })
581 .collect(),
582 }
583 }
584
585 fn into_record(self, head: Span) -> Value {
586 match self.groups {
587 Tree::Leaf(leaf) => Value::record(
588 leaf.into_iter()
589 .filter_map(|(k, v)| match k {
592 GroupKey::String(key) => Some((key, v.into_value(head))),
593 GroupKey::Nothing => None,
594 })
595 .collect(),
596 head,
597 ),
598 Tree::Branch(branch) => {
599 let values = branch
600 .into_iter()
601 .filter_map(|(k, v)| match k {
602 GroupKey::String(key) => Some((key, v.into_record(head))),
603 GroupKey::Nothing => None,
604 })
605 .collect();
606 Value::record(values, head)
607 }
608 }
609 }
610}
611
612#[cfg(test)]
613mod test {
614 use super::*;
615
616 #[test]
617 fn test_examples() -> nu_test_support::Result {
618 nu_test_support::test().examples(GroupBy)
619 }
620}