1use super::utils::chain_error_with_input;
2use nu_engine::{ClosureEval, ClosureEvalOnce, command_prelude::*};
3use nu_protocol::engine::Closure;
4
5#[derive(Clone)]
6pub struct Each;
7
8impl Command for Each {
9 fn name(&self) -> &str {
10 "each"
11 }
12
13 fn description(&self) -> &str {
14 "Run a closure on each row of the input list, creating a new list with the results."
15 }
16
17 fn extra_description(&self) -> &str {
18 r#"Since tables are lists of records, passing a table into 'each' will
19iterate over each record, not necessarily each cell within it.
20
21Avoid passing single records to this command. Since a record is a
22one-row structure, 'each' will only run once, behaving similar to 'do'.
23To iterate over a record's values, use 'items' or try converting it to a table
24with 'transpose' first.
25
26
27By default, for each input there is a single output value.
28If the closure returns a stream rather than value, the stream is collected
29completely, and the resulting value becomes one of the items in `each`'s output.
30
31To receive items from those streams without waiting for the whole stream to be
32collected, `each --flatten` can be used.
33Instead of waiting for the stream to be collected before returning the result as
34a single item, `each --flatten` will return each item as soon as they are received.
35
36This "flattens" the output, turning an output that would otherwise be a
37list of lists like `list<list<string>>` into a flat list like `list<string>`."#
38 }
39
40 fn search_terms(&self) -> Vec<&str> {
41 vec!["for", "loop", "iterate", "map"]
42 }
43
44 fn signature(&self) -> nu_protocol::Signature {
45 Signature::build("each")
46 .input_output_types(vec![
47 (
48 Type::List(Box::new(Type::Any)),
49 Type::List(Box::new(Type::Any)),
50 ),
51 (Type::table(), Type::List(Box::new(Type::Any))),
52 (Type::Any, Type::Any),
53 ])
54 .required(
55 "closure",
56 SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
57 "The closure to run.",
58 )
59 .switch("keep-empty", "keep empty result cells", Some('k'))
60 .switch(
61 "flatten",
62 "combine outputs into a single stream instead of\
63 collecting them to separate values",
64 Some('f'),
65 )
66 .allow_variants_without_examples(true)
67 .category(Category::Filters)
68 }
69
70 fn examples(&self) -> Vec<Example<'_>> {
71 vec![
72 Example {
73 example: "[1 2 3] | each {|e| 2 * $e }",
74 description: "Multiplies elements in the list",
75 result: Some(Value::test_list(vec![
76 Value::test_int(2),
77 Value::test_int(4),
78 Value::test_int(6),
79 ])),
80 },
81 Example {
82 example: "{major:2, minor:1, patch:4} | values | each {|| into string }",
83 description: "Produce a list of values in the record, converted to string",
84 result: Some(Value::test_list(vec![
85 Value::test_string("2"),
86 Value::test_string("1"),
87 Value::test_string("4"),
88 ])),
89 },
90 Example {
91 example: r#"[1 2 3 2] | each {|e| if $e == 2 { "two" } }"#,
92 description: "'null' items will be dropped from the result list. It has the same effect as 'filter_map' in other languages.",
93 result: Some(Value::test_list(vec![
94 Value::test_string("two"),
95 Value::test_string("two"),
96 ])),
97 },
98 Example {
99 example: r#"[1 2 3] | enumerate | each {|e| if $e.item == 2 { $"found 2 at ($e.index)!"} }"#,
100 description: "Iterate over each element, producing a list showing indexes of any 2s",
101 result: Some(Value::test_list(vec![Value::test_string("found 2 at 1!")])),
102 },
103 Example {
104 example: r#"[1 2 3] | each --keep-empty {|e| if $e == 2 { "found 2!"} }"#,
105 description: "Iterate over each element, keeping null results",
106 result: Some(Value::test_list(vec![
107 Value::nothing(Span::test_data()),
108 Value::test_string("found 2!"),
109 Value::nothing(Span::test_data()),
110 ])),
111 },
112 Example {
113 example: r#"$env.name? | each { $"hello ($in)" } | default "bye""#,
114 description: "Update value if not null, otherwise do nothing",
115 result: None,
116 },
117 Example {
118 description: "Scan through multiple files without pause",
119 example: "\
120 ls *.txt \
121 | each --flatten {|f| open $f.name | lines } \
122 | find -i 'note: ' \
123 | str join \"\\n\"\
124 ",
125 result: None,
126 },
127 ]
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 head = call.head;
138 let closure: Closure = call.req(engine_state, stack, 0)?;
139 let keep_empty = call.has_flag(engine_state, stack, "keep-empty")?;
140 let flatten = call.has_flag(engine_state, stack, "flatten")?;
141
142 let metadata = input.metadata();
143 let result = match input {
144 empty @ (PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, ..)) => {
145 return Ok(empty);
146 }
147 PipelineData::Value(Value::Range { .. }, ..)
148 | PipelineData::Value(Value::List { .. }, ..)
149 | PipelineData::ListStream(..) => {
150 let mut closure = ClosureEval::new(engine_state, stack, closure);
151
152 let out = if flatten {
153 input
154 .into_iter()
155 .flat_map(move |value| {
156 closure.run_with_value(value).unwrap_or_else(|error| {
157 Value::error(error, head).into_pipeline_data()
158 })
159 })
160 .into_pipeline_data(head, engine_state.signals().clone())
161 } else {
162 input
163 .into_iter()
164 .map(move |value| {
165 each_map(value, &mut closure, head)
166 .unwrap_or_else(|error| Value::error(error, head))
167 })
168 .into_pipeline_data(head, engine_state.signals().clone())
169 };
170 Ok(out)
171 }
172 PipelineData::ByteStream(stream, ..) => {
173 let Some(chunks) = stream.chunks() else {
174 return Ok(PipelineData::empty().set_metadata(metadata));
175 };
176
177 let mut closure = ClosureEval::new(engine_state, stack, closure);
178 let out = if flatten {
179 chunks
180 .flat_map(move |result| {
181 result
182 .and_then(|value| closure.run_with_value(value))
183 .unwrap_or_else(|error| {
184 Value::error(error, head).into_pipeline_data()
185 })
186 })
187 .into_pipeline_data(head, engine_state.signals().clone())
188 } else {
189 chunks
190 .map(move |result| {
191 result
192 .and_then(|value| each_map(value, &mut closure, head))
193 .unwrap_or_else(|error| Value::error(error, head))
194 })
195 .into_pipeline_data(head, engine_state.signals().clone())
196 };
197 Ok(out)
198 }
199 PipelineData::Value(value, ..) => {
202 ClosureEvalOnce::new(engine_state, stack, closure).run_with_value(value)
203 }
204 };
205
206 if keep_empty {
207 result
208 } else {
209 result.and_then(|x| x.filter(|v| !v.is_nothing(), engine_state.signals()))
210 }
211 .map(|data| data.set_metadata(metadata))
212 }
213}
214
215#[inline]
216fn each_map(value: Value, closure: &mut ClosureEval, head: Span) -> Result<Value, ShellError> {
217 let span = value.span();
218 let is_error = value.is_error();
219 closure
220 .run_with_value(value)
221 .and_then(|pipeline_data| pipeline_data.into_value(head))
222 .map_err(|error| chain_error_with_input(error, is_error, span))
223}
224
225#[cfg(test)]
226mod test {
227 use super::*;
228
229 #[test]
230 fn test_examples() {
231 use crate::test_examples;
232
233 test_examples(Each {})
234 }
235}