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(format!("{column}_{column}"), Value::list(vals, span));
239 } else {
240 out.insert(column, Value::list(vals, span));
241 }
242 } else if !columns.is_empty() {
243 let cell_path =
244 column_requested.and_then(|x| match x.members.first() {
245 Some(PathMember::String { val, .. }) => Some(val),
246 _ => None,
247 });
248
249 if let Some(r) = cell_path {
250 inner_table =
251 Some(TableInside::Entries(r.clone(), vals, column_index));
252 } else {
253 out.insert(column, Value::list(vals, span));
254 }
255 } else {
256 inner_table = Some(TableInside::Entries(column, vals, column_index));
257 }
258 }
259 _ => {
260 out.insert(column, value);
261 }
262 }
263 }
264
265 let mut expanded = vec![];
266 match inner_table {
267 Some(TableInside::Entries(column, entries, parent_column_index)) => {
268 for entry in entries {
269 let base = out.clone();
270 let mut record = Record::new();
271 let mut index = 0;
272 for (col, val) in base.into_iter() {
273 if index == parent_column_index {
276 record.push(column.clone(), entry.clone());
277 }
278 record.push(col, val);
279 index += 1;
280 }
281 if index == parent_column_index {
283 record.push(column.clone(), entry);
284 }
285 expanded.push(Value::record(record, tag));
286 }
287 }
288 Some(TableInside::FlattenedRows {
289 records,
290 parent_column_name,
291 parent_column_index,
292 }) => {
293 for inner_record in records {
294 let base = out.clone();
295 let mut record = Record::new();
296 let mut index = 0;
297
298 for (base_col, base_val) in base {
299 if index == parent_column_index {
302 for (col, val) in &inner_record {
303 if record.contains(col)
304 || (!columns.is_empty() && out.contains_key(col))
305 {
306 record.push(
307 format!("{parent_column_name}_{col}"),
308 val.clone(),
309 );
310 } else {
311 record.push(col, val.clone());
312 };
313 }
314 }
315
316 record.push(base_col, base_val);
317 index += 1;
318 }
319
320 if index == parent_column_index {
322 for (col, val) in inner_record {
323 if record.contains(&col)
324 || (!columns.is_empty() && out.contains_key(&col))
325 {
326 record.push(format!("{parent_column_name}_{col}"), val);
327 } else {
328 record.push(col, val);
329 }
330 }
331 }
332 expanded.push(Value::record(record, tag));
333 }
334 }
335 None => {
336 expanded.push(Value::record(out.into_iter().collect(), tag));
337 }
338 }
339 expanded
340 }
341 Value::List { vals, .. } => vals,
342 item => vec![item],
343 }
344}
345
346#[cfg(test)]
347mod test {
348 use super::*;
349 #[test]
350 fn test_examples() -> nu_test_support::Result {
351 nu_test_support::test().examples(Flatten)
352 }
353}