1use indexmap::IndexMap;
2use nu_engine::command_prelude::*;
3use nu_protocol::ast::PathMember;
4
5#[derive(Clone)]
6pub struct Flatten;
7
8impl Command for Flatten {
9 fn name(&self) -> &str {
10 "flatten"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("flatten")
15 .input_output_types(vec![
16 (
17 Type::List(Box::new(Type::Any)),
18 Type::List(Box::new(Type::Any)),
19 ),
20 (Type::record(), Type::table()),
21 ])
22 .rest(
23 "rest",
24 SyntaxShape::String,
25 "Optionally flatten data by column.",
26 )
27 .switch("all", "Flatten inner table one level out.", Some('a'))
28 .category(Category::Filters)
29 }
30
31 fn description(&self) -> &str {
32 "Flatten a table by extracting nested values."
33 }
34
35 fn run(
36 &self,
37 engine_state: &EngineState,
38 stack: &mut Stack,
39 call: &Call,
40 input: PipelineData,
41 ) -> Result<PipelineData, ShellError> {
42 flatten(engine_state, stack, call, input)
43 }
44
45 fn examples(&self) -> Vec<Example<'_>> {
46 vec![
47 Example {
48 description: "flatten a table.",
49 example: "[[N, u, s, h, e, l, l]] | flatten ",
50 result: Some(Value::test_list(vec![
51 Value::test_string("N"),
52 Value::test_string("u"),
53 Value::test_string("s"),
54 Value::test_string("h"),
55 Value::test_string("e"),
56 Value::test_string("l"),
57 Value::test_string("l"),
58 ])),
59 },
60 Example {
61 description: "flatten a table, get the first item.",
62 example: "[[N, u, s, h, e, l, l]] | flatten | first",
63 result: None, },
65 Example {
66 description: "flatten a column having a nested table.",
67 example: "[[origin, people]; [Ecuador, ([[name, meal]; ['Andres', 'arepa']])]] | flatten --all | get meal",
68 result: None, },
70 Example {
71 description: "restrict the flattening by passing column names.",
72 example: "[[origin, crate, versions]; [World, ([[name]; ['nu-cli']]), ['0.21', '0.22']]] | flatten versions --all | last | get versions",
73 result: None, },
75 Example {
76 description: "Flatten inner table.",
77 example: "{ a: b, d: [ 1 2 3 4 ], e: [ 4 3 ] } | flatten d --all",
78 result: Some(Value::list(
79 vec![
80 Value::test_record(record! {
81 "a" => Value::test_string("b"),
82 "d" => Value::test_int(1),
83 "e" => Value::test_list(
84 vec![Value::test_int(4), Value::test_int(3)],
85 ),
86 }),
87 Value::test_record(record! {
88 "a" => Value::test_string("b"),
89 "d" => Value::test_int(2),
90 "e" => Value::test_list(
91 vec![Value::test_int(4), Value::test_int(3)],
92 ),
93 }),
94 Value::test_record(record! {
95 "a" => Value::test_string("b"),
96 "d" => Value::test_int(3),
97 "e" => Value::test_list(
98 vec![Value::test_int(4), Value::test_int(3)],
99 ),
100 }),
101 Value::test_record(record! {
102 "a" => Value::test_string("b"),
103 "d" => Value::test_int(4),
104 "e" => Value::test_list(
105 vec![Value::test_int(4), Value::test_int(3)],
106 )
107 }),
108 ],
109 Span::test_data(),
110 )),
111 },
112 ]
113 }
114}
115
116fn flatten(
117 engine_state: &EngineState,
118 stack: &mut Stack,
119 call: &Call,
120 mut input: PipelineData,
121) -> Result<PipelineData, ShellError> {
122 let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
123 let metadata = input.take_metadata();
124 let flatten_all = call.has_flag(engine_state, stack, "all")?;
125
126 input
127 .flat_map(
128 move |item| flat_value(&columns, item, flatten_all),
129 engine_state.signals(),
130 )
131 .map(|x| x.set_metadata(metadata))
132}
133
134enum TableInside {
135 Entries(String, Vec<Value>, usize),
138 FlattenedRows {
144 records: Vec<Record>,
145 parent_column_name: String,
146 parent_column_index: usize,
147 },
148}
149
150fn flat_value(columns: &[CellPath], item: Value, all: bool) -> Vec<Value> {
151 let tag = item.span();
152
153 match item {
154 Value::Record { val, .. } => {
155 let val = val.into_owned();
156 let retained_outer_columns: Vec<String> = if columns.is_empty() {
157 vec![]
158 } else {
159 val.iter()
160 .filter_map(|(column, value)| {
161 let column_requested =
162 columns.iter().find(|c| c.to_column_name() == *column);
163 let need_flatten = column_requested.is_some();
164
165 let will_flatten = match value {
166 Value::Record { .. } => need_flatten,
167 Value::List { vals, .. } => {
168 if all && vals.iter().all(|value| value.as_record().is_ok()) {
169 need_flatten
170 } else {
171 matches!(
172 column_requested
173 .and_then(|cell_path| cell_path.members.first()),
174 Some(PathMember::String { .. })
175 )
176 }
177 }
178 _ => false,
179 };
180
181 (!will_flatten).then_some(column.clone())
182 })
183 .collect()
184 };
185 let mut out = IndexMap::<String, Value>::new();
186 let mut inner_table = None;
187
188 for (column_index, (column, value)) in val.into_iter().enumerate() {
189 let column_requested = columns.iter().find(|c| c.to_column_name() == column);
190 let need_flatten = { columns.is_empty() || column_requested.is_some() };
191 let span = value.span();
192
193 match value {
194 Value::Record { ref val, .. } => {
195 if need_flatten {
196 for (col, val) in val.clone().into_owned() {
197 if out.contains_key(&col)
198 || retained_outer_columns.iter().any(|column| column == &col)
199 {
200 out.insert(format!("{column}_{col}"), val);
201 } else {
202 out.insert(col, val);
203 }
204 }
205 } else if out.contains_key(&column) {
206 out.insert(format!("{column}_{column}"), value);
207 } else {
208 out.insert(column, value);
209 }
210 }
211 Value::List { vals, .. } => {
212 if need_flatten && inner_table.is_some() {
213 return vec![Value::error(
214 ShellError::UnsupportedInput {
215 msg: "can only flatten one inner list at a time. tried flattening more than one column with inner lists... but is flattened already".into(),
216 input: "value originates from here".into(),
217 msg_span: tag,
218 input_span: span
219 },
220 span,
221 )];
222 }
223
224 if all && vals.iter().all(|f| f.as_record().is_ok()) {
225 if need_flatten {
227 let records = vals
228 .into_iter()
229 .filter_map(|v| v.into_record().ok())
230 .collect();
231
232 inner_table = Some(TableInside::FlattenedRows {
233 records,
234 parent_column_name: column,
235 parent_column_index: column_index,
236 });
237 } else if out.contains_key(&column) {
238 out.insert(
239 format!("{column}_{column}"),
240 Value::list_shared(vals, span),
241 );
242 } else {
243 out.insert(column, Value::list_shared(vals, span));
244 }
245 } else if !columns.is_empty() {
246 let cell_path =
247 column_requested.and_then(|x| match x.members.first() {
248 Some(PathMember::String { val, .. }) => Some(val),
249 _ => None,
250 });
251
252 if let Some(r) = cell_path {
253 inner_table = Some(TableInside::Entries(
254 r.clone(),
255 vals.into_owned(),
256 column_index,
257 ));
258 } else {
259 out.insert(column, Value::list_shared(vals, span));
260 }
261 } else {
262 inner_table = Some(TableInside::Entries(
263 column,
264 vals.into_owned(),
265 column_index,
266 ));
267 }
268 }
269 _ => {
270 out.insert(column, value);
271 }
272 }
273 }
274
275 let mut expanded = vec![];
276 match inner_table {
277 Some(TableInside::Entries(column, entries, parent_column_index)) => {
278 for entry in entries {
279 let base = out.clone();
280 let mut record = Record::new();
281 let mut index = 0;
282 for (col, val) in base.into_iter() {
283 if index == parent_column_index {
286 record.push(column.clone(), entry.clone());
287 }
288 record.push(col, val);
289 index += 1;
290 }
291 if index == parent_column_index {
293 record.push(column.clone(), entry);
294 }
295 expanded.push(Value::record(record, tag));
296 }
297 }
298 Some(TableInside::FlattenedRows {
299 records,
300 parent_column_name,
301 parent_column_index,
302 }) => {
303 for inner_record in records {
304 let base = out.clone();
305 let mut record = Record::new();
306 let mut index = 0;
307
308 for (base_col, base_val) in base {
309 if index == parent_column_index {
312 for (col, val) in &inner_record {
313 if record.contains(col)
314 || (!columns.is_empty() && out.contains_key(col))
315 {
316 record.push(
317 format!("{parent_column_name}_{col}"),
318 val.clone(),
319 );
320 } else {
321 record.push(col, val.clone());
322 };
323 }
324 }
325
326 record.push(base_col, base_val);
327 index += 1;
328 }
329
330 if index == parent_column_index {
332 for (col, val) in inner_record {
333 if record.contains(&col)
334 || (!columns.is_empty() && out.contains_key(&col))
335 {
336 record.push(format!("{parent_column_name}_{col}"), val);
337 } else {
338 record.push(col, val);
339 }
340 }
341 }
342 expanded.push(Value::record(record, tag));
343 }
344 }
345 None => {
346 expanded.push(Value::record(out.into_iter().collect(), tag));
347 }
348 }
349 expanded
350 }
351 Value::List { vals, .. } => vals.into_owned(),
352 item => vec![item],
353 }
354}
355
356#[cfg(test)]
357mod test {
358 use super::*;
359 #[test]
360 fn test_examples() -> nu_test_support::Result {
361 nu_test_support::test().examples(Flatten)
362 }
363}